From 1b4fabf13542fd65fdd41c98d2ebcff5a1ceb82d Mon Sep 17 00:00:00 2001 From: Florent Morselli Date: Tue, 23 Apr 2024 21:24:33 +0200 Subject: [PATCH] Update dependencies and refactored some tests for code quality enhancement (#185) Reorganized dependencies and removed unused files for overall code optimization. Some enhancements to tests were made to improve quality assurance. Also incorporated rector checks and license checks via Github Actions workflows to ensure code integrity. --- .gitattributes | 7 +- .github/workflows/coding-standards.yml | 32 --- .github/workflows/exported_files.yml | 18 -- .github/workflows/infection.yml | 30 +++ .github/workflows/integrate.yml | 221 ++++++++++++++++++ .github/workflows/rector_checkstyle.yaml | 29 --- .github/workflows/static-analyze.yml | 32 --- .github/workflows/tests.yml | 32 --- .gitignore | 3 + Makefile | 52 ----- bin/build_javascript.js | 29 --- castor.php | 203 ++++++++++++++++ composer.json | 7 +- phpstan-baseline.neon | 110 --------- phpstan.neon | 3 + phpunit.xml.dist | 3 + src/Dto/Manifest.php | 5 - src/Resources/config/definition/manifest.php | 14 -- .../ManifestCompileEventListener.php | 10 +- src/Subscriber/PwaDevServerSubscriber.php | 20 +- src/Twig/PwaRuntime.php | 6 +- tests/DummyImageProcessor.php | 6 +- tests/Functional/AbstractPwaTestCase.php | 4 + tests/Functional/CompileCommandTest.php | 4 +- tests/Functional/GenerateIconsCommandTest.php | 2 + .../Functional/TakeScreenshotCommandTest.php | 4 + tests/config.php | 7 + 27 files changed, 519 insertions(+), 374 deletions(-) delete mode 100644 .github/workflows/coding-standards.yml delete mode 100644 .github/workflows/exported_files.yml create mode 100644 .github/workflows/infection.yml create mode 100644 .github/workflows/integrate.yml delete mode 100644 .github/workflows/rector_checkstyle.yaml delete mode 100644 .github/workflows/static-analyze.yml delete mode 100644 .github/workflows/tests.yml delete mode 100644 Makefile delete mode 100644 bin/build_javascript.js create mode 100644 castor.php diff --git a/.gitattributes b/.gitattributes index 5a2fbc7..a6e889f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -4,17 +4,12 @@ /tests export-ignore /.gitattributes export-ignore /.gitignore export-ignore +/castor.php export-ignore /CODE_OF_CONDUCT.md export-ignore /ecs.php export-ignore /infection.json.dist export-ignore /link export-ignore -/Makefile export-ignore /phpstan.neon export-ignore /phpstan-baseline.neon export-ignore /phpunit.xml.dist export-ignore /rector.php export-ignore -/bin export-ignore -/babel.config.js export-ignore -/jest.config.js export-ignore -/rollup.config.js export-ignore -/tsconfig.json export-ignore diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml deleted file mode 100644 index 7566d43..0000000 --- a/.github/workflows/coding-standards.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Coding Standards - -on: [push] - -jobs: - tests: - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: [ubuntu-latest] - php-versions: ['8.3'] - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: json, mbstring - coverage: xdebug - - - name: Install Composer dependencies - run: | - composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader - - - name: CODING STANDARDS - run: make ci-cs diff --git a/.github/workflows/exported_files.yml b/.github/workflows/exported_files.yml deleted file mode 100644 index 9669eea..0000000 --- a/.github/workflows/exported_files.yml +++ /dev/null @@ -1,18 +0,0 @@ -name: Exported files - -on: [push] - -jobs: - tests: - runs-on: ubuntu-latest - steps: - - name: "Checkout code" - uses: "actions/checkout@v4" - - - name: "Check exported files" - run: | - EXPECTED="LICENSE,README.md,RELEASES.md,SECURITY.md,composer.json" - CURRENT="$(git archive HEAD | tar --list --exclude="assets" --exclude="assets/*" --exclude="src" --exclude="src/*" --exclude="templates" --exclude="templates/*" | paste -s -d ",")" - echo "CURRENT =${CURRENT}" - echo "EXPECTED=${EXPECTED}" - test "${CURRENT}" == "${EXPECTED}" diff --git a/.github/workflows/infection.yml b/.github/workflows/infection.yml new file mode 100644 index 0000000..1e3b321 --- /dev/null +++ b/.github/workflows/infection.yml @@ -0,0 +1,30 @@ +name: "Infection" + +on: + push: + branches: + - "*.x" + +jobs: + mutation_testing: + name: "0️⃣ Mutation Testing" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.3" + extensions: "json, mbstring, sockets, gd, curl, imagick" + 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: "Execute Infection" + run: "castor infect" diff --git a/.github/workflows/integrate.yml b/.github/workflows/integrate.yml new file mode 100644 index 0000000..499a5d3 --- /dev/null +++ b/.github/workflows/integrate.yml @@ -0,0 +1,221 @@ +name: "Integrate" + +on: + push: + branches: + - "*.x" + pull_request: null + +jobs: + byte_level: + name: "0️⃣ Byte-level" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout code" + uses: "actions/checkout@v4" + + - name: "Check file permissions" + run: | + test "$(find . -type f -not -path './.git/*' -executable)" == "" + + - name: "Find non-printable ASCII characters" + run: | + ! LC_ALL=C.UTF-8 find . -type f -name "*.php" -print0 | xargs -0 -- grep -PHn "[^ -~]" + + syntax_errors: + name: "1️⃣ Syntax errors" + runs-on: "ubuntu-latest" + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.3" + extensions: "json, mbstring, sockets, gd, curl, imagick" + 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: "${{ 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: "castor lint" + + unit_tests: + name: "2️⃣ Unit and functional tests" + needs: + - "byte_level" + - "syntax_errors" + strategy: + matrix: + operating-system: + - "ubuntu-latest" + php-version: + - "8.2" + - "8.3" + dependencies: + - "highest" + runs-on: ${{ matrix.operating-system }} + steps: + - name: "Set up PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "${{ matrix.php-version }}" + extensions: "json, mbstring, sockets, gd, curl, imagick" + tools: castor + coverage: "xdebug" + + - name: "Checkout code" + uses: "actions/checkout@v4" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "${{ matrix.dependencies }}" + composer-options: "--optimize-autoloader" + + - name: "Execute unit tests" + run: "castor test --coverage-text" + + static_analysis: + name: "3️⃣ Static Analysis" + 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, sockets, gd, curl, imagick" + 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: "Execute static analysis" + run: "castor stan" + + coding_standards: + name: "4️⃣ Coding Standards" + 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, sockets, gd, curl, imagick" + 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, sockets, gd, curl, imagick" + 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 coding style" + run: "castor check-licenses" + + rector_checkstyle: + name: "6️⃣ Rector Checkstyle" + 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, sockets, gd, curl, imagick" + tools: castor + coverage: "xdebug" + + - name: "Checkout code" + uses: "actions/checkout@v4" + + - name: "Install dependencies" + uses: "ramsey/composer-install@v3" + with: + dependency-versions: "highest" + composer-options: "--optimize-autoloader" + + - name: "Execute Rector" + run: "castor rector" + + exported_files: + name: "7️⃣ Exported files" + needs: + - "byte_level" + - "syntax_errors" + runs-on: "ubuntu-20.04" + steps: + - name: "Checkout code" + uses: "actions/checkout@v4" + + - name: "Check exported files" + run: | + EXPECTED="LICENSE,README.md,RELEASES.md,SECURITY.md,composer.json" + CURRENT="$(git archive HEAD | tar --list --exclude="src" --exclude="src/*" --exclude="assets" --exclude="assets/*" --exclude="templates" --exclude="templates/*" | paste -s -d ",")" + echo "CURRENT =${CURRENT}" + echo "EXPECTED=${EXPECTED}" + test "${CURRENT}" == "${EXPECTED}" diff --git a/.github/workflows/rector_checkstyle.yaml b/.github/workflows/rector_checkstyle.yaml deleted file mode 100644 index fffbc46..0000000 --- a/.github/workflows/rector_checkstyle.yaml +++ /dev/null @@ -1,29 +0,0 @@ -name: Rector Checkstyle - -on: [push] - -jobs: - tests: - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: [ ubuntu-latest ] - php-versions: ['8.3'] - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: json, mbstring - coverage: none - - - name: Install Composer dependencies - run: composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader - - - name: Rector - run: make rector diff --git a/.github/workflows/static-analyze.yml b/.github/workflows/static-analyze.yml deleted file mode 100644 index 7808228..0000000 --- a/.github/workflows/static-analyze.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Static Analyze - -on: [push] - -jobs: - tests: - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: [ubuntu-latest] - php-versions: ['8.3'] - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: json, mbstring - coverage: xdebug - - - name: Install Composer dependencies - run: | - composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader - - - name: PHPStan - run: make st diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml deleted file mode 100644 index 11c944c..0000000 --- a/.github/workflows/tests.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Unit and Functional Tests - -on: [push] - -jobs: - tests: - runs-on: ${{ matrix.operating-system }} - strategy: - matrix: - operating-system: [ ubuntu-latest ] - php-versions: ['8.2', '8.3'] - name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.operating-system }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - with: - ref: ${{ github.head_ref }} - - - name: Setup PHP, with composer and extensions - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php-versions }} - extensions: json, mbstring - coverage: xdebug - - - name: Install Composer dependencies - run: | - composer update --no-progress --no-suggest --prefer-dist --optimize-autoloader - - - name: Run tests - run: make tests diff --git a/.gitignore b/.gitignore index b8e03bf..5da75f8 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ package-lock.json /composer.lock /vendor /.phpunit.cache/ +/drivers +/public +/.castor.stub.php \ No newline at end of file diff --git a/Makefile b/Makefile deleted file mode 100644 index 2e4596f..0000000 --- a/Makefile +++ /dev/null @@ -1,52 +0,0 @@ -.PHONY: tests -tests: vendor ## Run all tests - vendor/bin/phpunit --color - -.PHONY: code-coverage-html -cc: vendor ## Show test coverage rates (HTML) - vendor/bin/phpunit --coverage-html ./build - -.PHONY: cs -cs: vendor ## Fix all files using defined ECS rules - vendor/bin/ecs check --fix - -.PHONY: tu -tu: vendor ## Run only unit tests - vendor/bin/phpunit --color --group Unit - -.PHONY: ti -ti: vendor ## Run only integration tests - vendor/bin/phpunit --color --group Integration - -.PHONY: tf -tf: vendor ## Run only functional tests - vendor/bin/phpunit --color --group Functional - -.PHONY: st -st: vendor ## Run static analyse - vendor/bin/phpstan analyse - - -################################################ - -.PHONY: ci-cc -ci-cc: vendor ## Show test coverage rates (console) - -.PHONY: ci-cs -ci-cs: vendor ## Check all files using defined ECS rules - vendor/bin/ecs check - -################################################ - - -vendor: composer.json composer.lock - composer validate - composer install -.PHONY: rector -rector: vendor ## Check all files using Rector - vendor/bin/rector process --ansi --dry-run --xdebug - -.DEFAULT_GOAL := help -help: - @grep -E '(^[a-zA-Z_-]+:.*?##.*$$)|(^##)' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[32m%-30s\033[0m %s\n", $$1, $$2}' | sed -e 's/\[32m##/[33m/' -.PHONY: help diff --git a/bin/build_javascript.js b/bin/build_javascript.js deleted file mode 100644 index af28ddc..0000000 --- a/bin/build_javascript.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * This file is used to compile the TypeScript files in the assets/src directory - * of each package. - * - * It allows each package to spawn its own rollup process, which is necessary - * to keep memory usage down. - */ -const { spawnSync } = require('child_process'); -const glob = require('glob'); - -const files = [ - ...glob.sync('assets/src/*controller.ts'), -]; - -files.forEach((file) => { - const result = spawnSync('node', [ - 'node_modules/.bin/rollup', - '-c', - '--environment', - `INPUT_FILE:${file}`, - ], { - stdio: 'inherit', - shell: true - }); - - if (result.error) { - console.error(`Error compiling ${file}:`, result.error); - } -}); diff --git a/castor.php b/castor.php new file mode 100644 index 0000000..da1a11b --- /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/composer.json b/composer.json index 03d67db..3b8f8dc 100644 --- a/composer.json +++ b/composer.json @@ -46,17 +46,22 @@ "require-dev": { "ext-sockets": "*", "dbrekelmans/bdi": "^1.1", + "ekino/phpstan-banned-code": "^1.0", + "ergebnis/phpunit-slow-test-detector": "^2.14", "infection/infection": "^0.28", + "php-parallel-lint/php-parallel-lint": "^1.4", "phpstan/extension-installer": "^1.1", "phpstan/phpdoc-parser": "^1.28", "phpstan/phpstan": "^1.0", "phpstan/phpstan-beberlei-assert": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", - "phpstan/phpstan-phpunit": "^1.0", + "phpstan/phpstan-phpunit": "^1.3", "phpstan/phpstan-strict-rules": "^1.0", + "phpstan/phpstan-symfony": "^1.3", "phpunit/phpunit": "^10.1|^11.0", "rector/rector": "^1.0", "staabm/phpstan-todo-by": "^0.1.25", + "struggle-for-php/sfp-phpstan-psr-log": "^0.20.0", "symfony/filesystem": "^6.4|^7.0", "symfony/framework-bundle": "^6.4|^7.0", "symfony/mime": "^6.4|^7.0", diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b640631..e252646 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -460,91 +460,31 @@ parameters: count: 1 path: src/ImageProcessor/GDImageProcessor.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\AssetNormalizer\\:\\:denormalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/AssetNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\AssetNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/AssetNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\AssetNormalizer\\:\\:supportsDenormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/AssetNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\AssetNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/AssetNormalizer.php - - message: "#^PHPDoc tag @return with type array\\ is incompatible with native type string\\.$#" count: 1 path: src/Normalizer/AssetNormalizer.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\IconNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/IconNormalizer.php - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\IconNormalizer\\:\\:normalize\\(\\) should return array\\{src\\: string, sizes\\?\\: string, type\\?\\: string, purpose\\?\\: string\\} but returns array\\\\.$#" count: 1 path: src/Normalizer/IconNormalizer.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\IconNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/IconNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ScreenshotNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ScreenshotNormalizer.php - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ScreenshotNormalizer\\:\\:normalize\\(\\) should return array\\{src\\: string, sizes\\?\\: string, form_factor\\?\\: string, label\\?\\: string, platform\\?\\: string, format\\?\\: string\\} but returns array\\\\.$#" count: 1 path: src/Normalizer/ScreenshotNormalizer.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ScreenshotNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ScreenshotNormalizer.php - - message: "#^Parameter \\#1 \\$image of method SpomkyLabs\\\\PwaBundle\\\\ImageProcessor\\\\ImageProcessorInterface\\:\\:getSizes\\(\\) expects string, string\\|false given\\.$#" count: 1 path: src/Normalizer/ScreenshotNormalizer.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ServiceWorkerNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ServiceWorkerNormalizer.php - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ServiceWorkerNormalizer\\:\\:normalize\\(\\) should return array\\{scope\\?\\: string, src\\: string, use_cache\\?\\: bool\\} but returns array\\\\.$#" count: 1 path: src/Normalizer/ServiceWorkerNormalizer.php - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\ServiceWorkerNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/ServiceWorkerNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\UrlNormalizer\\:\\:normalize\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/UrlNormalizer.php - - - - message: "#^Method SpomkyLabs\\\\PwaBundle\\\\Normalizer\\\\UrlNormalizer\\:\\:supportsNormalization\\(\\) has parameter \\$context with no value type specified in iterable type array\\.$#" - count: 1 - path: src/Normalizer/UrlNormalizer.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" count: 1 @@ -665,56 +605,6 @@ parameters: count: 3 path: src/Resources/config/definition/service_worker.php - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:end\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/file_handlers.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/icons.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:beforeNormalization\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/launch_handler.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:append\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/protocol_handlers.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:append\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/related_applications.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/screenshots.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:scalarNode\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/shared_target.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:scalarNode\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/shortcuts.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/url_node.php - - - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeParentInterface\\:\\:scalarNode\\(\\)\\.$#" - count: 1 - path: src/Resources/config/definition/utils/widgets.php - - message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition\\:\\:children\\(\\)\\.$#" count: 1 diff --git a/phpstan.neon b/phpstan.neon index 102b06a..d550a2c 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -2,10 +2,13 @@ parameters: level: max paths: - src + - tests checkMissingIterableValueType: true checkGenericClassInNonGenericObjectType: true checkUninitializedProperties: true treatPhpDocTypesAsCertain: false + scanFiles: + - vendor/symfony/dependency-injection/Loader/Configurator/ContainerConfigurator.php includes: - vendor/phpstan/phpstan/conf/bleedingEdge.neon - phpstan-baseline.neon diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3d7f866..4772da6 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -28,4 +28,7 @@ ./tests + + + diff --git a/src/Dto/Manifest.php b/src/Dto/Manifest.php index a947bcb..775c594 100644 --- a/src/Dto/Manifest.php +++ b/src/Dto/Manifest.php @@ -61,11 +61,6 @@ final class Manifest #[SerializedName('iarc_rating_id')] public null|string $iarcRatingId = null; - /** - * @var array - */ - public array $locales = []; - /** * @var array */ diff --git a/src/Resources/config/definition/manifest.php b/src/Resources/config/definition/manifest.php index abf866c..cd8eeb9 100644 --- a/src/Resources/config/definition/manifest.php +++ b/src/Resources/config/definition/manifest.php @@ -20,16 +20,6 @@ ->children() ->arrayNode('manifest') ->canBeEnabled() - ->validate() - ->ifTrue( - static fn (array $v) => count( - $v['locales'] - ) !== 0 && ! str_contains((string) $v['public_url'], '{locale}') - ) - ->thenInvalid( - 'When setting locales, the public URL "public_url" must contain the "{locale}" placeholder.' - ) - ->end() ->children() ->scalarNode('public_url') ->defaultValue('/site.webmanifest') @@ -41,10 +31,6 @@ ->defaultTrue() ->info('Indicates whether the manifest should be fetched with credentials.') ->end() - ->arrayNode('locales') - ->defaultValue([]) - ->scalarPrototype()->end() - ->end() ->scalarNode('background_color') ->info( 'The background color of the application. It should match the background-color CSS property in the sites stylesheet for a smooth transition between launching the web application and loading the site\'s content.' diff --git a/src/Subscriber/ManifestCompileEventListener.php b/src/Subscriber/ManifestCompileEventListener.php index 8fa182e..6f677fa 100644 --- a/src/Subscriber/ManifestCompileEventListener.php +++ b/src/Subscriber/ManifestCompileEventListener.php @@ -34,6 +34,9 @@ private array $jsonOptions; + /** + * @param array $locales + */ public function __construct( private SerializerInterface $serializer, private Manifest $manifest, @@ -44,6 +47,8 @@ public function __construct( #[Autowire('%kernel.debug%')] bool $debug, null|EventDispatcherInterface $dispatcher, + #[Autowire('%kernel.enabled_locales%')] + private array $locales, ) { $this->dispatcher = $dispatcher ?? new NullEventDispatcher(); $this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/'); @@ -65,10 +70,11 @@ public function __invoke(PreAssetsCompileEvent $event): void return; } $manifest = clone $this->manifest; - if (count($this->manifest->locales) === 0) { + + if (count($this->locales) === 0 || ! str_contains($this->manifestPublicUrl, '{locale}')) { $this->compileManifest($manifest, null); } else { - foreach ($this->manifest->locales as $locale) { + foreach ($this->locales as $locale) { $this->compileManifest($manifest, $locale); } } diff --git a/src/Subscriber/PwaDevServerSubscriber.php b/src/Subscriber/PwaDevServerSubscriber.php index 4ec65a7..010b70e 100644 --- a/src/Subscriber/PwaDevServerSubscriber.php +++ b/src/Subscriber/PwaDevServerSubscriber.php @@ -49,6 +49,9 @@ private array $jsonOptions; + /** + * @param array $locales + */ public function __construct( private ServiceWorkerCompiler $serviceWorkerBuilder, private SerializerInterface $serializer, @@ -60,6 +63,8 @@ public function __construct( #[Autowire('%kernel.debug%')] bool $debug, null|EventDispatcherInterface $dispatcher, + #[Autowire('%kernel.enabled_locales%')] + private array $locales, ) { $this->dispatcher = $dispatcher ?? new NullEventDispatcher(); $this->manifestPublicUrl = '/' . trim($manifestPublicUrl, '/'); @@ -94,19 +99,24 @@ public function onKernelRequest(RequestEvent $event): void $request = $event->getRequest(); $pathInfo = $request->getPathInfo(); $localizedManifestPublicUrls = []; - foreach ($this->manifest->locales as $locale) { + foreach ($this->locales as $locale) { $localizedManifestPublicUrls[$locale] = str_replace('{locale}', $locale, $this->manifestPublicUrl); } switch (true) { - case $this->manifest->enabled === true && in_array($pathInfo, $localizedManifestPublicUrls, true): + case $this->manifest->enabled === true && str_contains($this->manifestPublicUrl, '{locale}') && in_array( + $pathInfo, + $localizedManifestPublicUrls, + true + ): $locale = array_search($pathInfo, $localizedManifestPublicUrls, true); assert(is_string($locale), 'Locale not found.'); $this->serveManifest($event, $locale); break; - case $this->manifest->enabled === true && count( - $localizedManifestPublicUrls - ) === 0 && $pathInfo === $this->manifestPublicUrl: + case $this->manifest->enabled === true && ! str_contains( + $this->manifestPublicUrl, + '{locale}' + ) && $pathInfo === $this->manifestPublicUrl: $this->serveManifest($event); break; case $this->manifest->serviceWorker?->enabled === true && $pathInfo === $this->serviceWorkerPublicUrl: diff --git a/src/Twig/PwaRuntime.php b/src/Twig/PwaRuntime.php index 67bc202..71d0f6d 100644 --- a/src/Twig/PwaRuntime.php +++ b/src/Twig/PwaRuntime.php @@ -62,12 +62,8 @@ private function injectManifestFile(string $output, null|string $locale): string if ($this->manifest->useCredentials === true) { $useCredentials = ' crossorigin="use-credentials"'; } - $hreflang = ''; - if ($locale !== null) { - $hreflang = sprintf(' hreflang="%s"', mb_strtolower(str_replace('_', '-', $locale))); - } - return $output . sprintf('%s', PHP_EOL, $url, $useCredentials, $hreflang); + return $output . sprintf('%s', PHP_EOL, $url, $useCredentials); } private function injectThemeColor(string $output, bool $themeColor): string diff --git a/tests/DummyImageProcessor.php b/tests/DummyImageProcessor.php index 8eb07f1..4e6a858 100644 --- a/tests/DummyImageProcessor.php +++ b/tests/DummyImageProcessor.php @@ -5,6 +5,7 @@ namespace SpomkyLabs\PwaBundle\Tests; use SpomkyLabs\PwaBundle\ImageProcessor\ImageProcessorInterface; +use function assert; /** * @internal @@ -13,11 +14,14 @@ class DummyImageProcessor implements ImageProcessorInterface { public function process(string $image, ?int $width, ?int $height, ?string $format): string { - return json_encode([ + $json = json_encode([ 'width' => $width, 'height' => $height, 'format' => $format, ]); + assert($json !== false); + + return $json; } /** diff --git a/tests/Functional/AbstractPwaTestCase.php b/tests/Functional/AbstractPwaTestCase.php index 87ef9e1..64a27b5 100644 --- a/tests/Functional/AbstractPwaTestCase.php +++ b/tests/Functional/AbstractPwaTestCase.php @@ -7,6 +7,7 @@ use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Component\Filesystem\Filesystem; +use function assert; /** * @internal @@ -18,6 +19,7 @@ abstract class AbstractPwaTestCase extends KernelTestCase protected function setUp(): void { self::cleanupFolder(); + assert(self::$kernel !== null); self::$application = new Application(self::$kernel); parent::setUp(); } @@ -30,7 +32,9 @@ protected function tearDown(): void private static function cleanupFolder(): void { + assert(self::$kernel !== null); $filesystem = self::getContainer()->get(Filesystem::class); + assert($filesystem instanceof Filesystem); $filesystem->remove(sprintf('%s/samples', self::$kernel->getCacheDir())); $filesystem->remove(sprintf('%s/output', self::$kernel->getCacheDir())); } diff --git a/tests/Functional/CompileCommandTest.php b/tests/Functional/CompileCommandTest.php index 51a1f54..d4d3233 100644 --- a/tests/Functional/CompileCommandTest.php +++ b/tests/Functional/CompileCommandTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; +use function assert; /** * @internal @@ -13,11 +14,12 @@ final class CompileCommandTest extends AbstractPwaTestCase { #[Test] - public static function aScreenshotIsCorrectlyTake(): void + public static function theFileAreCompiled(): void { // Given $command = self::$application->find('asset-map:compile'); $commandTester = new CommandTester($command); + assert(self::$kernel !== null); // When $commandTester->execute([]); diff --git a/tests/Functional/GenerateIconsCommandTest.php b/tests/Functional/GenerateIconsCommandTest.php index 1bb69a8..77b16c6 100644 --- a/tests/Functional/GenerateIconsCommandTest.php +++ b/tests/Functional/GenerateIconsCommandTest.php @@ -6,6 +6,7 @@ use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; +use function assert; /** * @internal @@ -18,6 +19,7 @@ public static function iconsAreCorrectlyCreated(): void // Given $command = self::$application->find('pwa:create:icons'); $commandTester = new CommandTester($command); + assert(self::$kernel !== null); $output = sprintf('%s/samples/icons', self::$kernel->getCacheDir()); // When diff --git a/tests/Functional/TakeScreenshotCommandTest.php b/tests/Functional/TakeScreenshotCommandTest.php index d054764..b8dd723 100644 --- a/tests/Functional/TakeScreenshotCommandTest.php +++ b/tests/Functional/TakeScreenshotCommandTest.php @@ -4,8 +4,10 @@ namespace SpomkyLabs\PwaBundle\Tests\Functional; +use Ergebnis\PHPUnit\SlowTestDetector\Attribute\MaximumDuration; use PHPUnit\Framework\Attributes\Test; use Symfony\Component\Console\Tester\CommandTester; +use function assert; /** * @internal @@ -13,11 +15,13 @@ final class TakeScreenshotCommandTest extends AbstractPwaTestCase { #[Test] + #[MaximumDuration(1500)] public static function aScreenshotIsCorrectlyTake(): void { // Given $command = self::$application->find('pwa:create:screenshot'); $commandTester = new CommandTester($command); + assert(self::$kernel !== null); $output = sprintf('%s/samples/screenshots/', self::$kernel->getCacheDir()); // When diff --git a/tests/config.php b/tests/config.php index 38c09c6..b811592 100644 --- a/tests/config.php +++ b/tests/config.php @@ -27,8 +27,12 @@ 'test' => true, 'secret' => 'test', 'http_method_override' => true, + 'handle_all_throwables' => true, 'session' => [ 'storage_factory_id' => 'session.storage.factory.mock_file', + 'cookie_secure' => 'auto', + 'cookie_samesite' => 'lax', + 'handler_id' => 'session.handler.native_file', ], 'asset_mapper' => [ 'paths' => [ @@ -45,6 +49,9 @@ 'default_path' => '%kernel.project_dir%/tests/translations', 'fallbacks' => ['en'], ], + 'php_errors' => [ + 'log' => true, + ], ]); $container->extension('pwa', [ 'image_processor' => DummyImageProcessor::class,