diff --git a/.gitattributes b/.gitattributes index f9856b07..a32de845 100644 --- a/.gitattributes +++ b/.gitattributes @@ -10,6 +10,7 @@ /.gitsplit.yml export-ignore /monorepo-builder.php export-ignore /CODE_OF_CONDUCT.md export-ignore +/castor.php export-ignore /deptrac.yaml export-ignore /ecs.php export-ignore /infection.json export-ignore diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml index cd32d9eb..cd6ed23e 100644 --- a/.github/workflows/infection.yml +++ b/.github/workflows/infection.yml @@ -1,4 +1,4 @@ -name: "Integrate" +name: "Infection" on: push: @@ -7,7 +7,7 @@ on: jobs: mutation_testing: - name: "5️⃣ Mutation Testing" + name: "0️⃣ Mutation Testing" runs-on: "ubuntu-latest" steps: - name: "Set up PHP" @@ -15,13 +15,11 @@ jobs: with: php-version: "8.3" extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor - name: "Checkout code" uses: "actions/checkout@v4" - - name: "Fetch Git base reference" - run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" - - name: "Install dependencies" uses: "ramsey/composer-install@v3" with: @@ -29,4 +27,4 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Infection" - run: "make ci-mu" + run: "castor infect" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml index beed9326..80a20b1a 100644 --- a/.github/workflows/integrate.yml +++ b/.github/workflows/integrate.yml @@ -30,17 +30,33 @@ jobs: uses: "shivammathur/setup-php@v2" with: php-version: "8.3" + extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor - name: "Checkout code" uses: "actions/checkout@v4" + - name: "Fetch Git base reference" + run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" + + - name: "Validate Composer configuration" + run: "castor validate" + - name: "Install dependencies" uses: "ramsey/composer-install@v3" with: - dependency-versions: "highest" + dependency-versions: "${{ matrix.dependencies }}" + composer-options: "--optimize-autoloader" + + - name: "Cache dependencies" + uses: "actions/cache@v4" + id: "cache" + with: + path: "composer-cache" + key: "${{ runner.os }}-${{ hashFiles('**/composer.json') }}" - name: "Check source code for syntax errors" - run: "composer exec -- parallel-lint src/ tests/" + run: "castor lint" unit_tests: name: "2️⃣ Unit and functional tests" @@ -63,6 +79,7 @@ jobs: with: php-version: "${{ matrix.php-version }}" extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor coverage: "xdebug" - name: "Checkout code" @@ -75,7 +92,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute unit tests" - run: "make ci-cc" + run: "castor test --coverage-text" static_analysis: name: "3️⃣ Static Analysis" @@ -89,13 +106,11 @@ jobs: with: php-version: "8.3" extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor - name: "Checkout code" uses: "actions/checkout@v4" - - name: "Validate Composer configuration" - run: "composer validate --strict" - - name: "Install dependencies" uses: "ramsey/composer-install@v3" with: @@ -103,7 +118,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute static analysis" - run: "make st" + run: "castor stan" coding_standards: name: "4️⃣ Coding Standards" @@ -117,13 +132,40 @@ jobs: with: php-version: "8.3" extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor - name: "Checkout code" uses: "actions/checkout@v4" + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--optimize-autoloader" + - name: "Check adherence to EditorConfig" uses: "greut/eclint-action@v0" + - name: "Check coding style" + run: "castor cs" + + check_licenses: + name: "5️⃣ Check licenses" + needs: + - "byte_level" + - "syntax_errors" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.3" + extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor + + - name: "Checkout code" + uses: "actions/checkout@v4" + - name: "Install dependencies" uses: "ramsey/composer-install@v3" with: @@ -131,7 +173,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Check coding style" - run: "make ci-cs" + run: "castor check-licenses" rector_checkstyle: name: "6️⃣ Rector Checkstyle" @@ -145,14 +187,12 @@ jobs: with: php-version: "8.3" extensions: "json, mbstring, openssl, sqlite3, curl, uuid" + tools: castor coverage: "xdebug" - name: "Checkout code" uses: "actions/checkout@v4" - - name: "Fetch Git base reference" - run: "git fetch --depth=1 origin ${GITHUB_BASE_REF}" - - name: "Install dependencies" uses: "ramsey/composer-install@v3" with: @@ -160,7 +200,7 @@ jobs: composer-options: "--optimize-autoloader" - name: "Execute Rector" - run: "make rector" + run: "castor rector" exported_files: name: "7️⃣ Exported files" diff --git a/.gitignore b/.gitignore index 287a6d9e..277c2fc2 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ composer.lock vendor/ src/Bundle/JoseFramework/var/ infection.txt +.castor.stub.php diff --git a/castor.php b/castor.php new file mode 100644 index 00000000..da1a11b6 --- /dev/null +++ b/castor.php @@ -0,0 +1,203 @@ +title('Running infection'); + $nproc = run('nproc', quiet: true); + if (! $nproc->isSuccessful()) { + io()->error('Cannot determine the number of processors'); + return; + } + $threads = (int) $nproc->getOutput(); + $command = [ + 'php', + 'vendor/bin/infection', + sprintf('--min-msi=%s', $minMsi), + sprintf('--min-covered-msi=%s', $minCoveredMsi), + sprintf('--threads=%s', $threads), + ]; + if ($ci) { + $command[] = '--logger-github'; + $command[] = '-s'; + } + $environment = [ + 'XDEBUG_MODE' => 'coverage', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run tests')] +function test(bool $coverageHtml = false, bool $coverageText = false, null|string $group = null): void +{ + io()->title('Running tests'); + $command = ['php', 'vendor/bin/phpunit', '--color']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + if ($coverageHtml) { + $command[] = '--coverage-html=build/coverage'; + $environment['XDEBUG_MODE'] = 'coverage'; + } + if ($coverageText) { + $command[] = '--coverage-text'; + $environment['XDEBUG_MODE'] = 'coverage'; + } + if ($group !== null) { + $command[] = sprintf('--group=%s', $group); + } + run($command, environment: $environment); +} + +#[AsTask(description: 'Coding standards check')] +function cs( + #[\Castor\Attribute\AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[\Castor\Attribute\AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running coding standards check'); + $command = ['php', 'vendor/bin/ecs', 'check']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + if ($fix) { + $command[] = '--fix'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + run($command, environment: $environment); +} + +#[AsTask(description: 'Running PHPStan')] +function stan(): void +{ + io()->title('Running PHPStan'); + $command = ['php', 'vendor/bin/phpstan', 'analyse']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Validate Composer configuration')] +function validate(): void +{ + io()->title('Validating Composer configuration'); + $command = ['composer', 'validate', '--strict']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); + + $command = ['composer', 'dump-autoload', '--optimize', '--strict-psr']; + run($command, environment: $environment); +} + +/** + * @param array $allowedLicenses + */ +#[AsTask(description: 'Check licenses')] +function checkLicenses( + array $allowedLicenses = ['Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MIT', 'MPL-2.0', 'OSL-3.0'] +): void { + io()->title('Checking licenses'); + $allowedExceptions = []; + $command = ['composer', 'licenses', '-f', 'json']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + $result = run($command, environment: $environment, quiet: true); + if (! $result->isSuccessful()) { + io()->error('Cannot determine licenses'); + exit(1); + } + $licenses = json_decode($result->getOutput(), true); + $disallowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => ! in_array($name, $allowedExceptions, true) + && count(array_diff($info['license'], $allowedLicenses)) === 1, + \ARRAY_FILTER_USE_BOTH + ); + $allowed = array_filter( + $licenses['dependencies'], + static fn (array $info, $name) => in_array($name, $allowedExceptions, true) + || count(array_diff($info['license'], $allowedLicenses)) === 0, + \ARRAY_FILTER_USE_BOTH + ); + if (count($disallowed) > 0) { + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($disallowed), + $disallowed + ) + ); + io() + ->error('Disallowed licenses found'); + exit(1); + } + io() + ->table( + ['Package', 'License'], + array_map( + static fn ($name, $info) => [$name, implode(', ', $info['license'])], + array_keys($allowed), + $allowed + ) + ); + io() + ->success('All licenses are allowed'); +} + +#[AsTask(description: 'Run Rector')] +function rector( + #[\Castor\Attribute\AsOption(description: 'Fix issues if possible')] + bool $fix = false, + #[\Castor\Attribute\AsOption(description: 'Clear cache')] + bool $clearCache = false +): void { + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/rector', 'process', '--ansi']; + if (! $fix) { + $command[] = '--dry-run'; + } + if ($clearCache) { + $command[] = '--clear-cache'; + } + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run Rector')] +function deptrac(): void +{ + io()->title('Running Rector'); + $command = ['php', 'vendor/bin/deptrac', 'analyse', '--fail-on-uncovered', '--no-cache']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} + +#[AsTask(description: 'Run Linter')] +function lint(): void +{ + io()->title('Running Linter'); + $command = ['composer', 'exec', '--', 'parallel-lint', __DIR__ . '/src/', __DIR__ . '/tests/']; + $environment = [ + 'XDEBUG_MODE' => 'off', + ]; + run($command, environment: $environment); +} diff --git a/ecs.php b/ecs.php index c0440103..40e74a0d 100644 --- a/ecs.php +++ b/ecs.php @@ -90,11 +90,6 @@ $config->skip([PhpUnitTestClassRequiresCoversFixer::class]); $config->parallel(); - $config->paths([ - __DIR__ . '/performance', - __DIR__ . '/src', - __DIR__ . '/tests', - __DIR__ . '/ecs.php', - __DIR__ . '/rector.php', - ]); + $config->paths([__DIR__]); + $config->skip([__DIR__ . '/.github', __DIR__ . '/.castor.stub.php', __DIR__ . '/var', __DIR__ . '/vendor']); }; diff --git a/rector.php b/rector.php index 0f5ae450..c6bc2810 100644 --- a/rector.php +++ b/rector.php @@ -5,18 +5,15 @@ use Rector\Config\RectorConfig; use Rector\Doctrine\Set\DoctrineSetList; use Rector\PHPUnit\CodeQuality\Rector\Class_\PreferPHPUnitThisCallRector; -use Rector\PHPUnit\Set\PHPUnitLevelSetList; use Rector\PHPUnit\Set\PHPUnitSetList; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; -use Rector\Symfony\Set\SymfonyLevelSetList; use Rector\Symfony\Set\SymfonySetList; use Rector\ValueObject\PhpVersion; return static function (RectorConfig $config): void { $config->import(SetList::DEAD_CODE); $config->import(LevelSetList::UP_TO_PHP_83); - //$config->import(SymfonyLevelSetList::UP_TO_SYMFONY_64); $config->import(SymfonySetList::SYMFONY_64); $config->import(SymfonySetList::SYMFONY_50_TYPES); $config->import(SymfonySetList::SYMFONY_52_VALIDATOR_ATTRIBUTES); @@ -28,7 +25,6 @@ $config->import(PHPUnitSetList::PHPUNIT_CODE_QUALITY); $config->import(PHPUnitSetList::ANNOTATIONS_TO_ATTRIBUTES); $config->import(PHPUnitSetList::PHPUNIT_100); - //$config->import(PHPUnitLevelSetList::UP_TO_PHPUNIT_100); $config->paths([ __DIR__ . '/ecs.php', __DIR__ . '/rector.php', diff --git a/src/Library/Checker/AlgorithmChecker.php b/src/Library/Checker/AlgorithmChecker.php index 20afadee..23cb606a 100644 --- a/src/Library/Checker/AlgorithmChecker.php +++ b/src/Library/Checker/AlgorithmChecker.php @@ -9,8 +9,9 @@ use function is_string; /** - * This class is a header parameter checker. When the "alg" header parameter is present, it will check if the value is - * within the allowed ones. + * AlgorithmChecker class. + * + * This class implements the HeaderChecker interface and is responsible for checking the "alg" header in a token. */ final readonly class AlgorithmChecker implements HeaderChecker { diff --git a/src/Library/Checker/AudienceChecker.php b/src/Library/Checker/AudienceChecker.php index 3d608500..e7396a56 100644 --- a/src/Library/Checker/AudienceChecker.php +++ b/src/Library/Checker/AudienceChecker.php @@ -10,8 +10,7 @@ use function is_string; /** - * This class is a header parameter and claim checker. When the "aud" header parameter or claim is present, it will - * check if the value is within the allowed ones. + * Represents a class that checks the audience claim and header in a JWT token. */ final readonly class AudienceChecker implements ClaimChecker, HeaderChecker { diff --git a/src/Library/Checker/CallableChecker.php b/src/Library/Checker/CallableChecker.php index ceb0020f..ff5212a0 100644 --- a/src/Library/Checker/CallableChecker.php +++ b/src/Library/Checker/CallableChecker.php @@ -10,6 +10,7 @@ use function is_callable; /** + * This class is responsible for checking claims and headers using a callable function. * @see \Jose\Tests\Component\Checker\CallableCheckerTest */ final class CallableChecker implements ClaimChecker, HeaderChecker diff --git a/src/Library/Checker/ClaimChecker.php b/src/Library/Checker/ClaimChecker.php index 771f6d3f..3f97a1f3 100644 --- a/src/Library/Checker/ClaimChecker.php +++ b/src/Library/Checker/ClaimChecker.php @@ -4,16 +4,19 @@ namespace Jose\Component\Checker; +/** + * Represents a claim checker interface. + * Claim checkers are responsible for validating claims on a token. + */ interface ClaimChecker { /** - * When the token has the applicable claim, the value is checked. If for some reason the value is not valid, an - * InvalidClaimException must be thrown. + * Checks if the given value matches the claim. */ public function checkClaim(mixed $value): void; /** - * The method returns the claim to be checked. + * Returns the supported claim. */ public function supportedClaim(): string; } diff --git a/src/Library/Checker/ClaimCheckerManager.php b/src/Library/Checker/ClaimCheckerManager.php index 64f0c405..330a471e 100644 --- a/src/Library/Checker/ClaimCheckerManager.php +++ b/src/Library/Checker/ClaimCheckerManager.php @@ -8,8 +8,7 @@ use function count; /** - * This manager handles as many claim checkers as needed. - * + * This class manages claim checkers and performs claim checks. * @see \Jose\Tests\Component\Checker\ClaimCheckerManagerTest */ class ClaimCheckerManager diff --git a/src/Library/Checker/ClaimCheckerManagerFactory.php b/src/Library/Checker/ClaimCheckerManagerFactory.php index f6c45ac9..8e994d6f 100644 --- a/src/Library/Checker/ClaimCheckerManagerFactory.php +++ b/src/Library/Checker/ClaimCheckerManagerFactory.php @@ -7,6 +7,7 @@ use InvalidArgumentException; /** + * This class is responsible for creating and managing claim checkers. * @see \Jose\Tests\Component\Checker\ClaimCheckerManagerFactoryTest */ class ClaimCheckerManagerFactory diff --git a/src/Library/Checker/ClaimExceptionInterface.php b/src/Library/Checker/ClaimExceptionInterface.php index 47971d03..5101a3ad 100644 --- a/src/Library/Checker/ClaimExceptionInterface.php +++ b/src/Library/Checker/ClaimExceptionInterface.php @@ -7,7 +7,10 @@ use Throwable; /** - * Exceptions thrown by this component. + * Represents an interface for claim exceptions. + * + * This interface extends from the Throwable interface, allowing + * the claim exceptions to be thrown and caught like any other exception. */ interface ClaimExceptionInterface extends Throwable { diff --git a/src/Library/Checker/ExpirationTimeChecker.php b/src/Library/Checker/ExpirationTimeChecker.php index da2b192f..9eeaede7 100644 --- a/src/Library/Checker/ExpirationTimeChecker.php +++ b/src/Library/Checker/ExpirationTimeChecker.php @@ -10,7 +10,9 @@ use function is_int; /** - * This class is a claim checker. When the "exp" is present, it will compare the value with the current timestamp. + * This class is a claim checker. + * + * When the "exp" is present, it will compare the value with the current timestamp. */ final readonly class ExpirationTimeChecker implements ClaimChecker, HeaderChecker { diff --git a/src/Library/Checker/HeaderChecker.php b/src/Library/Checker/HeaderChecker.php index 0856235b..daffe765 100644 --- a/src/Library/Checker/HeaderChecker.php +++ b/src/Library/Checker/HeaderChecker.php @@ -4,21 +4,25 @@ namespace Jose\Component\Checker; +/** + * Interface HeaderChecker + * + * This interface defines the contract for a header checker. + */ interface HeaderChecker { /** - * This method is called when the header parameter is present. If for some reason the value is not valid, an - * InvalidHeaderException must be thrown. + * Checks if the given value matches the header parameter of the token. */ public function checkHeader(mixed $value): void; /** - * The method returns the header parameter to be checked. + * Retrieves the supported header for the token. */ public function supportedHeader(): string; /** - * When true, the header parameter to be checked MUST be set in the protected header of the token. + * Returns a boolean value indicating whether the requested resource can only be accessed with a protected header. */ public function protectedHeaderOnly(): bool; } diff --git a/src/Library/Checker/HeaderCheckerManager.php b/src/Library/Checker/HeaderCheckerManager.php index 1b05be6e..427c9b85 100644 --- a/src/Library/Checker/HeaderCheckerManager.php +++ b/src/Library/Checker/HeaderCheckerManager.php @@ -10,6 +10,12 @@ use function count; use function is_array; +/** + * This class is a factory to create Header Checker Managers. + * + * It allows to add header parameter checkers and token type supports. + * The factory is responsible to create a Header Checker Manager with the header parameter checkers found based + */ class HeaderCheckerManager { /** diff --git a/src/Library/Checker/HeaderCheckerManagerFactory.php b/src/Library/Checker/HeaderCheckerManagerFactory.php index c514050d..17339639 100644 --- a/src/Library/Checker/HeaderCheckerManagerFactory.php +++ b/src/Library/Checker/HeaderCheckerManagerFactory.php @@ -7,6 +7,9 @@ use InvalidArgumentException; /** + * This class is a factory to create Header Checker Managers. It allows to add header parameter checkers and token type + * supports. The factory is responsible to create a Header Checker Manager with the header parameter checkers found based + * on the alias. If the alias is not supported, an InvalidArgumentException is thrown. * @see \Jose\Tests\Component\Checker\HeaderCheckerManagerFactoryTest */ class HeaderCheckerManagerFactory diff --git a/src/Library/Checker/InvalidHeaderException.php b/src/Library/Checker/InvalidHeaderException.php index ae3e1d8b..9e70590d 100644 --- a/src/Library/Checker/InvalidHeaderException.php +++ b/src/Library/Checker/InvalidHeaderException.php @@ -7,7 +7,7 @@ use Exception; /** - * This exception is thrown by header parameter checkers when a header parameter check failed. + * This exception is thrown by header checkers when a header check failed. */ class InvalidHeaderException extends Exception { diff --git a/src/Library/Checker/IsEqualChecker.php b/src/Library/Checker/IsEqualChecker.php index 7709aa0d..c39ee9db 100644 --- a/src/Library/Checker/IsEqualChecker.php +++ b/src/Library/Checker/IsEqualChecker.php @@ -7,6 +7,7 @@ use Override; /** + * This class implements a claim and header checker that checks if the value is equal to the expected value. * @see \Jose\Tests\Component\Checker\IsEqualCheckerTest */ final readonly class IsEqualChecker implements ClaimChecker, HeaderChecker diff --git a/src/Library/Checker/IssuerChecker.php b/src/Library/Checker/IssuerChecker.php index e4651b4c..b5db0b98 100644 --- a/src/Library/Checker/IssuerChecker.php +++ b/src/Library/Checker/IssuerChecker.php @@ -9,8 +9,9 @@ use function is_string; /** - * This class is a header parameter and claim checker. When the "iss" header parameter or claim is present, it will - * check if the value is within the allowed ones. + * This class is a header parameter and claim checker. + * + * When the "iss" header parameter or claim is present, it will check if the value is within the allowed ones. */ final readonly class IssuerChecker implements ClaimChecker, HeaderChecker { diff --git a/src/Library/Checker/MissingMandatoryClaimException.php b/src/Library/Checker/MissingMandatoryClaimException.php index f58bb2cb..85c00a83 100644 --- a/src/Library/Checker/MissingMandatoryClaimException.php +++ b/src/Library/Checker/MissingMandatoryClaimException.php @@ -6,6 +6,9 @@ use Exception; +/** + * This exception is thrown by claim checkers when a mandatory claim is missing. + */ class MissingMandatoryClaimException extends Exception implements ClaimExceptionInterface { /** diff --git a/src/Library/Checker/TokenTypeSupport.php b/src/Library/Checker/TokenTypeSupport.php index 96b179b9..e280c9ed 100644 --- a/src/Library/Checker/TokenTypeSupport.php +++ b/src/Library/Checker/TokenTypeSupport.php @@ -6,6 +6,13 @@ use Jose\Component\Core\JWT; +/** + * This interface is used to support token types. + * + * The token type is a way to define the format of the token. + * For example, the JWE token type is used to define the format of the token when it is encrypted. + * The JWS token type is used to define the format of the token when it is signed. + */ interface TokenTypeSupport { /**