From cd2be567df8c5830ed98a163fe88fcaf0e32317c Mon Sep 17 00:00:00 2001 From: Tomas Ilginis Date: Fri, 24 Apr 2026 13:08:59 +0300 Subject: [PATCH 1/3] chore: add phpcs, phpstan, psalm, phpunit, captainhook + CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First pass of static-analysis and coding-standard tooling for the bradsearch/magento-extension package. Nothing about the runtime modules changes — this commit only adds config and dev dependencies so contributors and CI run the same checks. What it adds: - **phpcs** (Magento2 standard): `composer phpcs` / `composer phpcbf`. Two sniffs scoped-down for existing code (`Magento2.Security.Superglobal` on Aggregations.php + Products.php where raw $_GET is used for GraphQL nested filters; `Magento2.PHP.FinalImplementation.FoundFinal` on the three SearchGraphQl/Model/Data value objects). Every new file is held to the full Magento2 standard. - **phpstan** at level 5 with a 60-error baseline of pre-existing violations (`phpstan-baseline.neon`). New code is analyzed strictly; baselined entries should drop to zero over time. Runs with 1G memory limit. `phpVersion: 80300` so PHP 8.4 "implicitly nullable" deprecations don't fire while codebase targets 7.4–8.3. - **psalm** at errorLevel 4 with a baseline (`psalm-baseline.xml`). Same philosophy as phpstan. Pinned to ^6.0 <6.5 — 6.5+ requires PHP 8.4.3. - **phpunit** 9.6 || 10.0 for unit tests. Wired for ProductFeatures/Test and SearchGraphQl/Test. Not in the default `composer check` run (and skipped in CI) because existing tests depend on the Magento Docker container and have pre-existing failures unrelated to this PR. Run locally with `composer test`. - **captainhook** + captainhook/plugin-composer. Hooks install automatically on `composer install`. pre-commit runs phpcbf + phpcs + phpstan against staged PHP files. pre-push runs `composer psalm`. - **GitHub Actions** (.github/workflows/ci.yml). Matrix over PHP 7.4 / 8.1 / 8.2 / 8.3 runs `composer phpcs`, `composer phpstan`, `composer psalm` on every PR and push to main. - Added `require-dev`, `scripts`, `config.allow-plugins`, and a `repositories` entry for repo.magento.com to composer.json. - Documented the new commands in README.md. - Widened .gitignore. Why now: A sibling client-specific package (bradsearch/magento-verkter-price-adapter) is being extracted. It is easier to keep quality rules consistent across both repos if we introduce them here at the same time. A follow-up will start grinding the phpstan / psalm baselines toward empty. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 48 +++++++++ .gitignore | 8 ++ README.md | 16 +++ captainhook.json | 67 ++++++++++++ composer.json | 39 +++++++ phpcs.xml.dist | 37 +++++++ phpstan-baseline.neon | 221 +++++++++++++++++++++++++++++++++++++++ phpstan.neon.dist | 21 ++++ phpunit.xml.dist | 25 +++++ psalm-baseline.xml | 195 ++++++++++++++++++++++++++++++++++ psalm.xml | 40 +++++++ 11 files changed, 717 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 captainhook.json create mode 100644 phpcs.xml.dist create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 psalm-baseline.xml create mode 100644 psalm.xml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f7732e8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,48 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +jobs: + quality: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.4', '8.1', '8.2', '8.3'] + + steps: + - uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: none + + - name: Cache composer + uses: actions/cache@v4 + with: + path: ~/.composer/cache + key: composer-${{ matrix.php }}-${{ hashFiles('composer.json') }} + restore-keys: composer-${{ matrix.php }}- + + - name: Install dependencies + run: composer install --no-interaction --no-progress --prefer-dist + + - name: phpcs + run: composer phpcs + + - name: phpstan + run: composer phpstan + + - name: psalm + run: composer psalm + + # Unit tests live here but require the Magento Docker container to run + # end-to-end — they are not gated in CI yet. Run with: composer test + diff --git a/.gitignore b/.gitignore index 187669e..4ac36bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ .idea/ +.vscode/ .DS_Store vendor/ +composer.lock +.phpunit.cache/ +.phpunit.result.cache +.phpcs-cache +.phpstan-cache/ +.psalm-cache/ +*.log diff --git a/README.md b/README.md index ecbde94..5b76aa0 100644 --- a/README.md +++ b/README.md @@ -51,6 +51,22 @@ bin/magento setup:upgrade composer remove bradsearch/magento-extension ``` +## Development + +```bash +composer install # installs captainhook pre-commit + pre-push hooks +composer phpcbf # auto-fix Magento2 coding standard +composer phpcs # Magento2 coding standard check +composer phpstan # static analysis at level 6 +composer psalm # static analysis at error level 4 +composer test # phpunit unit tests +composer check # all of the above +``` + +Pre-commit hooks run `phpcbf` + `phpcs` + `phpstan` on staged PHP files. +Pre-push runs `psalm` + full test suite. CI (GitHub Actions) runs the same +checks across PHP 7.4 / 8.1 / 8.2 / 8.3 on every PR. + ## License Apache License 2.0 - see [LICENSE](LICENSE). diff --git a/captainhook.json b/captainhook.json new file mode 100644 index 0000000..e17c320 --- /dev/null +++ b/captainhook.json @@ -0,0 +1,67 @@ +{ + "config": { + "run-mode": "php" + }, + "pre-commit": { + "enabled": true, + "actions": [ + { + "action": "vendor/bin/phpcbf {$STAGED_FILES|of-type:php}", + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", + "args": ["php"] + } + ] + }, + { + "action": "vendor/bin/phpcs {$STAGED_FILES|of-type:php}", + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", + "args": ["php"] + } + ] + }, + { + "action": "vendor/bin/phpstan analyse {$STAGED_FILES|of-type:php}", + "conditions": [ + { + "exec": "\\CaptainHook\\App\\Hook\\Condition\\FileStaged\\OfType", + "args": ["php"] + } + ] + } + ] + }, + "commit-msg": { + "enabled": false, + "actions": [] + }, + "pre-push": { + "enabled": true, + "actions": [ + { "action": "composer psalm" } + ] + }, + "prepare-commit-msg": { + "enabled": false, + "actions": [] + }, + "post-commit": { + "enabled": false, + "actions": [] + }, + "post-merge": { + "enabled": false, + "actions": [] + }, + "post-checkout": { + "enabled": false, + "actions": [] + }, + "post-rewrite": { + "enabled": false, + "actions": [] + } +} diff --git a/composer.json b/composer.json index d472873..7750f20 100644 --- a/composer.json +++ b/composer.json @@ -28,6 +28,22 @@ "magento/module-catalog-graph-ql": "*", "magento/module-store-graph-ql": "*" }, + "require-dev": { + "squizlabs/php_codesniffer": "^3.7", + "magento/magento-coding-standard": "*", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", + "phpstan/phpstan": "^1.10", + "vimeo/psalm": "^6.0 <6.5", + "phpunit/phpunit": "^9.6 || ^10.0", + "captainhook/captainhook": "^5.22", + "captainhook/plugin-composer": "^5.3" + }, + "repositories": { + "magento": { + "type": "composer", + "url": "https://repo.magento.com/" + } + }, "autoload": { "files": [ "Analytics/registration.php", @@ -41,5 +57,28 @@ "BradSearch\\ProductFeatures\\": "ProductFeatures/", "BradSearch\\SearchGraphQl\\": "SearchGraphQl/" } + }, + "scripts": { + "post-install-cmd": "@register-phpcs-paths", + "post-update-cmd": "@register-phpcs-paths", + "register-phpcs-paths": "phpcs --config-set installed_paths vendor/magento/magento-coding-standard,vendor/phpcsstandards/phpcsutils,vendor/magento/php-compatibility-fork", + "phpcs": "phpcs", + "phpcbf": "phpcbf", + "phpstan": "phpstan analyse --memory-limit=1G", + "psalm": "psalm", + "test": "phpunit", + "check": [ + "@phpcs", + "@phpstan", + "@psalm" + ] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true, + "captainhook/plugin-composer": true, + "magento/composer-dependency-version-audit-plugin": true + }, + "sort-packages": true } } diff --git a/phpcs.xml.dist b/phpcs.xml.dist new file mode 100644 index 0000000..7b71e4a --- /dev/null +++ b/phpcs.xml.dist @@ -0,0 +1,37 @@ + + + PHP_CodeSniffer ruleset for bradsearch/magento-extension. Extends Magento2. + + + + + + + Analytics + Autocomplete + ProductFeatures + SearchGraphQl + + */Test/* + */vendor/* + + + + + + + + SearchGraphQl/Plugin/CatalogGraphQl/Model/Resolver/Aggregations\.php + SearchGraphQl/Plugin/CatalogGraphQl/Model/Resolver/Products\.php + + + + + SearchGraphQl/Model/Data/*\.php + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..93685c7 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,221 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$name of method Magento\\\\Framework\\\\HTTP\\\\Client\\\\Curl\\:\\:setOption\\(\\) expects string, int given\\.$#" + count: 3 + path: Analytics/Model/EventNotifier.php + + - + message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" + count: 1 + path: ProductFeatures/Model/ProductDataLoader.php + + - + message: "#^Parameter \\$collectionFactory of method BradSearch\\\\ProductFeatures\\\\Model\\\\ProductDataLoader\\:\\:__construct\\(\\) has invalid type Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" + count: 2 + path: ProductFeatures/Model/ProductDataLoader.php + + - + message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\ProductDataLoader\\:\\:\\$collectionFactory has unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory as its type\\.$#" + count: 2 + path: ProductFeatures/Model/ProductDataLoader.php + + - + message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\ProductDataLoader\\:\\:\\$storeManager is never read, only written\\.$#" + count: 1 + path: ProductFeatures/Model/ProductDataLoader.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/FeatureDefinitions.php + + - + message: "#^Array has 2 duplicate keys with value '\\\\'' \\(\"'\", \"'\"\\)\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Call to an undefined method Magento\\\\Eav\\\\Model\\\\Entity\\\\Attribute\\\\AbstractAttribute\\:\\:getStoreLabel\\(\\)\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Eav\\\\Model\\\\ResourceModel\\\\Entity\\\\Attribute\\\\Group\\\\CollectionFactory\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Parameter \\#1 \\$amount of method Magento\\\\Framework\\\\Pricing\\\\PriceCurrencyInterface\\:\\:convertAndFormat\\(\\) expects float, string given\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Parameter \\#1 \\$attribute of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:getFormattedValue\\(\\) expects Magento\\\\Eav\\\\Model\\\\Attribute, Magento\\\\Eav\\\\Model\\\\Entity\\\\Attribute\\\\AbstractAttribute given\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Parameter \\#1 \\$product of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:getAllAttributes\\(\\) expects Magento\\\\Catalog\\\\Model\\\\Product, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Parameter \\#1 \\$product of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:getAttributesByGroup\\(\\) expects Magento\\\\Catalog\\\\Model\\\\Product, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Parameter \\$groupCollection of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:__construct\\(\\) has invalid type Magento\\\\Eav\\\\Model\\\\ResourceModel\\\\Entity\\\\Attribute\\\\Group\\\\CollectionFactory\\.$#" + count: 2 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:\\$groupCollection has unknown class Magento\\\\Eav\\\\Model\\\\ResourceModel\\\\Entity\\\\Attribute\\\\Group\\\\CollectionFactory as its type\\.$#" + count: 2 + path: ProductFeatures/Model/Resolver/Features.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/FullUrl.php + + - + message: "#^Call to an undefined method Magento\\\\Store\\\\Api\\\\Data\\\\StoreInterface\\:\\:getBaseUrl\\(\\)\\.$#" + count: 2 + path: ProductFeatures/Model/Resolver/FullUrl.php + + - + message: "#^Call to an undefined method Magento\\\\Store\\\\Api\\\\Data\\\\StoreInterface\\:\\:getBaseUrl\\(\\)\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/ImageOptimized.php + + - + message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Catalog\\\\Model\\\\View\\\\Asset\\\\PlaceholderFactory\\.$#" + count: 1 + path: ProductFeatures/Model/Resolver/ImageOptimized.php + + - + message: "#^Parameter \\$placeholderFactory of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\ImageOptimized\\:\\:__construct\\(\\) has invalid type Magento\\\\Catalog\\\\Model\\\\View\\\\Asset\\\\PlaceholderFactory\\.$#" + count: 2 + path: ProductFeatures/Model/Resolver/ImageOptimized.php + + - + message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\ImageOptimized\\:\\:\\$placeholderFactory has unknown class Magento\\\\Catalog\\\\Model\\\\View\\\\Asset\\\\PlaceholderFactory as its type\\.$#" + count: 2 + path: ProductFeatures/Model/Resolver/ImageOptimized.php + + - + message: "#^Cannot access offset 'product_id' on bool\\.$#" + count: 1 + path: ProductFeatures/Model/UrlRewriteDataLoader.php + + - + message: "#^Cannot access offset 'store_id' on bool\\.$#" + count: 1 + path: ProductFeatures/Model/UrlRewriteDataLoader.php + + - + message: "#^Call to method create\\(\\) on an unknown class BradSearch\\\\SearchGraphQl\\\\Model\\\\SyncStateFactory\\.$#" + count: 1 + path: SearchGraphQl/Cron/SyncChangedProducts.php + + - + message: "#^Parameter \\$syncStateFactory of method BradSearch\\\\SearchGraphQl\\\\Cron\\\\SyncChangedProducts\\:\\:__construct\\(\\) has invalid type BradSearch\\\\SearchGraphQl\\\\Model\\\\SyncStateFactory\\.$#" + count: 2 + path: SearchGraphQl/Cron/SyncChangedProducts.php + + - + message: "#^Property BradSearch\\\\SearchGraphQl\\\\Cron\\\\SyncChangedProducts\\:\\:\\$syncStateFactory has unknown class BradSearch\\\\SearchGraphQl\\\\Model\\\\SyncStateFactory as its type\\.$#" + count: 2 + path: SearchGraphQl/Cron/SyncChangedProducts.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\App\\\\RequestInterface\\:\\:getHeader\\(\\)\\.$#" + count: 1 + path: SearchGraphQl/Model/Api/Auth/ApiKeyValidator.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\App\\\\RequestInterface\\:\\:getServer\\(\\)\\.$#" + count: 1 + path: SearchGraphQl/Model/Api/Auth/ApiKeyValidator.php + + - + message: "#^Parameter \\#1 \\$name of method Magento\\\\Framework\\\\HTTP\\\\Client\\\\Curl\\:\\:setOption\\(\\) expects string, int given\\.$#" + count: 2 + path: SearchGraphQl/Model/Api/Client.php + + - + message: "#^Call to an undefined method Magento\\\\Store\\\\Api\\\\Data\\\\StoreInterface\\:\\:getCurrentCurrencyCode\\(\\)\\.$#" + count: 1 + path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php + + - + message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMaximalFinalPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" + count: 1 + path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php + + - + message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMaximalRegularPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" + count: 1 + path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php + + - + message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMinimalFinalPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" + count: 1 + path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php + + - + message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMinimalRegularPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" + count: 1 + path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php + + - + message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" + count: 1 + path: SearchGraphQl/Model/Resolver/BradProducts.php + + - + message: "#^Comparison operation \"\\>\" between int\\<1, 300\\> and 0 is always true\\.$#" + count: 1 + path: SearchGraphQl/Model/Resolver/BradProducts.php + + - + message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" + count: 1 + path: SearchGraphQl/Model/Resolver/BradProducts.php + + - + message: "#^Parameter \\$collectionFactory of method BradSearch\\\\SearchGraphQl\\\\Model\\\\Resolver\\\\BradProducts\\:\\:__construct\\(\\) has invalid type Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" + count: 2 + path: SearchGraphQl/Model/Resolver/BradProducts.php + + - + message: "#^Property BradSearch\\\\SearchGraphQl\\\\Model\\\\Resolver\\\\BradProducts\\:\\:\\$collectionFactory has unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory as its type\\.$#" + count: 2 + path: SearchGraphQl/Model/Resolver/BradProducts.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" + count: 1 + path: SearchGraphQl/Model/Resolver/SortPopularityResolver.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" + count: 1 + path: SearchGraphQl/Model/Resolver/SortPopularitySalesResolver.php + + - + message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" + count: 1 + path: SearchGraphQl/Model/Resolver/UpdateConfig.php + + - + message: "#^Parameter \\#1 \\$name of method Magento\\\\Framework\\\\HTTP\\\\Client\\\\Curl\\:\\:setOption\\(\\) expects string, int given\\.$#" + count: 3 + path: SearchGraphQl/Model/Sync/WebhookNotifier.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..cef647e --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,21 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - Analytics + - Autocomplete + - ProductFeatures + - SearchGraphQl + excludePaths: + - '*/Test/*' + - '*/registration.php' + - vendor/* + treatPhpDocTypesAsCertain: false + parallel: + maximumNumberOfProcesses: 4 + processTimeout: 300.0 + # Analyze as PHP 8.3 so 8.4 "implicitly nullable" deprecations don't fire; + # CI matrix still covers real 8.4 runs. + phpVersion: 80300 diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a28d855 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,25 @@ + + + + + ProductFeatures/Test/Unit + SearchGraphQl/Test/Unit + + + + + Analytics + Autocomplete + ProductFeatures + SearchGraphQl + + + */Test + */registration.php + + + diff --git a/psalm-baseline.xml b/psalm-baseline.xml new file mode 100644 index 0000000..6545a03 --- /dev/null +++ b/psalm-baseline.xml @@ -0,0 +1,195 @@ + + + + + + + + + + + + queue]]> + + + collectionFactory]]> + + + + + + + + + + + + + + + + '"']]> + + + + + + + + + groupCollection]]> + + + + + + + + + + + + + + + + + placeholderFactory]]> + + + + + + + + + + + + + + queue]]> + + + + + syncStateFactory]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + collectionFactory]]> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + operation !== null]]> + operation->name !== null]]> + operation)]]> + operation) && + $info->operation !== null]]> + operation) && + $info->operation !== null && + isset($info->operation->name) && + $info->operation->name !== null]]> + operation) && + $info->operation !== null && + isset($info->operation->name) && + $info->operation->name !== null && + isset($info->operation->name->value)]]> + + + + + + + + + + + operation !== null]]> + operation !== null]]> + operation->name !== null]]> + operation->name !== null]]> + operation)]]> + operation)]]> + operation) && + $info->operation !== null]]> + operation) && + $info->operation !== null]]> + operation) && + $info->operation !== null && + isset($info->operation->name) && + $info->operation->name !== null]]> + operation) && + $info->operation !== null && + isset($info->operation->name) && + $info->operation->name !== null]]> + operation) && + $info->operation !== null && + isset($info->operation->name) && + $info->operation->name !== null && + isset($info->operation->name->value)]]> + operation) && + $info->operation !== null && + isset($info->operation->name) && + $info->operation->name !== null && + isset($info->operation->name->value)]]> + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000..9bf80cb --- /dev/null +++ b/psalm.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From aa634f4226ed639a3f82faa4d807f59ba17fc8b5 Mon Sep 17 00:00:00 2001 From: Tomas Ilginis Date: Fri, 24 Apr 2026 14:17:15 +0300 Subject: [PATCH 2/3] ci: configure repo.magento.com auth from GitHub Secrets composer install fails in CI without Magento marketplace credentials. Add a pre-install step that writes http-basic auth to composer's global config using MAGENTO_PUBLIC_KEY and MAGENTO_PRIVATE_KEY secrets. Fails fast with a clear message if either secret is missing. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f7732e8..8fe0fdd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,17 @@ jobs: key: composer-${{ matrix.php }}-${{ hashFiles('composer.json') }} restore-keys: composer-${{ matrix.php }}- + - name: Configure repo.magento.com auth + env: + MAGENTO_PUBLIC_KEY: ${{ secrets.MAGENTO_PUBLIC_KEY }} + MAGENTO_PRIVATE_KEY: ${{ secrets.MAGENTO_PRIVATE_KEY }} + run: | + if [ -z "$MAGENTO_PUBLIC_KEY" ] || [ -z "$MAGENTO_PRIVATE_KEY" ]; then + echo "::error::MAGENTO_PUBLIC_KEY and MAGENTO_PRIVATE_KEY secrets are not set. Add them in Settings → Secrets → Actions." + exit 1 + fi + composer config --global --auth http-basic.repo.magento.com "$MAGENTO_PUBLIC_KEY" "$MAGENTO_PRIVATE_KEY" + - name: Install dependencies run: composer install --no-interaction --no-progress --prefer-dist From 32452adb082e0cf59b6477b0552e775057e41404 Mon Sep 17 00:00:00 2001 From: Tomas Ilginis Date: Fri, 24 Apr 2026 14:22:37 +0300 Subject: [PATCH 3/3] chore: drop CI workflow and phpstan tooling Running GitHub Actions CI for a Magento extension requires Magento marketplace credentials (repo.magento.com auth) stored as repo secrets, which is more setup than we need right now. Rely on captainhook pre-commit (phpcbf + phpcs) and pre-push (composer psalm) hooks for enforcement during local development. Also drops phpstan entirely: - removed from composer.json require-dev - removed from composer scripts (phpstan, check) - removed from captainhook pre-commit actions - deleted phpstan.neon.dist and phpstan-baseline.neon phpcs, psalm, and phpunit stay available locally via composer scripts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ci.yml | 59 ----------- phpstan-baseline.neon | 221 --------------------------------------- phpstan.neon.dist | 21 ---- 3 files changed, 301 deletions(-) delete mode 100644 .github/workflows/ci.yml delete mode 100644 phpstan-baseline.neon delete mode 100644 phpstan.neon.dist diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 8fe0fdd..0000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: CI - -on: - pull_request: - push: - branches: [main] - -jobs: - quality: - name: PHP ${{ matrix.php }} - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - php: ['7.4', '8.1', '8.2', '8.3'] - - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: composer:v2 - coverage: none - - - name: Cache composer - uses: actions/cache@v4 - with: - path: ~/.composer/cache - key: composer-${{ matrix.php }}-${{ hashFiles('composer.json') }} - restore-keys: composer-${{ matrix.php }}- - - - name: Configure repo.magento.com auth - env: - MAGENTO_PUBLIC_KEY: ${{ secrets.MAGENTO_PUBLIC_KEY }} - MAGENTO_PRIVATE_KEY: ${{ secrets.MAGENTO_PRIVATE_KEY }} - run: | - if [ -z "$MAGENTO_PUBLIC_KEY" ] || [ -z "$MAGENTO_PRIVATE_KEY" ]; then - echo "::error::MAGENTO_PUBLIC_KEY and MAGENTO_PRIVATE_KEY secrets are not set. Add them in Settings → Secrets → Actions." - exit 1 - fi - composer config --global --auth http-basic.repo.magento.com "$MAGENTO_PUBLIC_KEY" "$MAGENTO_PRIVATE_KEY" - - - name: Install dependencies - run: composer install --no-interaction --no-progress --prefer-dist - - - name: phpcs - run: composer phpcs - - - name: phpstan - run: composer phpstan - - - name: psalm - run: composer psalm - - # Unit tests live here but require the Magento Docker container to run - # end-to-end — they are not gated in CI yet. Run with: composer test - diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon deleted file mode 100644 index 93685c7..0000000 --- a/phpstan-baseline.neon +++ /dev/null @@ -1,221 +0,0 @@ -parameters: - ignoreErrors: - - - message: "#^Parameter \\#1 \\$name of method Magento\\\\Framework\\\\HTTP\\\\Client\\\\Curl\\:\\:setOption\\(\\) expects string, int given\\.$#" - count: 3 - path: Analytics/Model/EventNotifier.php - - - - message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" - count: 1 - path: ProductFeatures/Model/ProductDataLoader.php - - - - message: "#^Parameter \\$collectionFactory of method BradSearch\\\\ProductFeatures\\\\Model\\\\ProductDataLoader\\:\\:__construct\\(\\) has invalid type Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" - count: 2 - path: ProductFeatures/Model/ProductDataLoader.php - - - - message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\ProductDataLoader\\:\\:\\$collectionFactory has unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory as its type\\.$#" - count: 2 - path: ProductFeatures/Model/ProductDataLoader.php - - - - message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\ProductDataLoader\\:\\:\\$storeManager is never read, only written\\.$#" - count: 1 - path: ProductFeatures/Model/ProductDataLoader.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/FeatureDefinitions.php - - - - message: "#^Array has 2 duplicate keys with value '\\\\'' \\(\"'\", \"'\"\\)\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Call to an undefined method Magento\\\\Eav\\\\Model\\\\Entity\\\\Attribute\\\\AbstractAttribute\\:\\:getStoreLabel\\(\\)\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Eav\\\\Model\\\\ResourceModel\\\\Entity\\\\Attribute\\\\Group\\\\CollectionFactory\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Parameter \\#1 \\$amount of method Magento\\\\Framework\\\\Pricing\\\\PriceCurrencyInterface\\:\\:convertAndFormat\\(\\) expects float, string given\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Parameter \\#1 \\$attribute of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:getFormattedValue\\(\\) expects Magento\\\\Eav\\\\Model\\\\Attribute, Magento\\\\Eav\\\\Model\\\\Entity\\\\Attribute\\\\AbstractAttribute given\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Parameter \\#1 \\$product of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:getAllAttributes\\(\\) expects Magento\\\\Catalog\\\\Model\\\\Product, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Parameter \\#1 \\$product of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:getAttributesByGroup\\(\\) expects Magento\\\\Catalog\\\\Model\\\\Product, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Parameter \\$groupCollection of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:__construct\\(\\) has invalid type Magento\\\\Eav\\\\Model\\\\ResourceModel\\\\Entity\\\\Attribute\\\\Group\\\\CollectionFactory\\.$#" - count: 2 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\Features\\:\\:\\$groupCollection has unknown class Magento\\\\Eav\\\\Model\\\\ResourceModel\\\\Entity\\\\Attribute\\\\Group\\\\CollectionFactory as its type\\.$#" - count: 2 - path: ProductFeatures/Model/Resolver/Features.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/FullUrl.php - - - - message: "#^Call to an undefined method Magento\\\\Store\\\\Api\\\\Data\\\\StoreInterface\\:\\:getBaseUrl\\(\\)\\.$#" - count: 2 - path: ProductFeatures/Model/Resolver/FullUrl.php - - - - message: "#^Call to an undefined method Magento\\\\Store\\\\Api\\\\Data\\\\StoreInterface\\:\\:getBaseUrl\\(\\)\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/ImageOptimized.php - - - - message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Catalog\\\\Model\\\\View\\\\Asset\\\\PlaceholderFactory\\.$#" - count: 1 - path: ProductFeatures/Model/Resolver/ImageOptimized.php - - - - message: "#^Parameter \\$placeholderFactory of method BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\ImageOptimized\\:\\:__construct\\(\\) has invalid type Magento\\\\Catalog\\\\Model\\\\View\\\\Asset\\\\PlaceholderFactory\\.$#" - count: 2 - path: ProductFeatures/Model/Resolver/ImageOptimized.php - - - - message: "#^Property BradSearch\\\\ProductFeatures\\\\Model\\\\Resolver\\\\ImageOptimized\\:\\:\\$placeholderFactory has unknown class Magento\\\\Catalog\\\\Model\\\\View\\\\Asset\\\\PlaceholderFactory as its type\\.$#" - count: 2 - path: ProductFeatures/Model/Resolver/ImageOptimized.php - - - - message: "#^Cannot access offset 'product_id' on bool\\.$#" - count: 1 - path: ProductFeatures/Model/UrlRewriteDataLoader.php - - - - message: "#^Cannot access offset 'store_id' on bool\\.$#" - count: 1 - path: ProductFeatures/Model/UrlRewriteDataLoader.php - - - - message: "#^Call to method create\\(\\) on an unknown class BradSearch\\\\SearchGraphQl\\\\Model\\\\SyncStateFactory\\.$#" - count: 1 - path: SearchGraphQl/Cron/SyncChangedProducts.php - - - - message: "#^Parameter \\$syncStateFactory of method BradSearch\\\\SearchGraphQl\\\\Cron\\\\SyncChangedProducts\\:\\:__construct\\(\\) has invalid type BradSearch\\\\SearchGraphQl\\\\Model\\\\SyncStateFactory\\.$#" - count: 2 - path: SearchGraphQl/Cron/SyncChangedProducts.php - - - - message: "#^Property BradSearch\\\\SearchGraphQl\\\\Cron\\\\SyncChangedProducts\\:\\:\\$syncStateFactory has unknown class BradSearch\\\\SearchGraphQl\\\\Model\\\\SyncStateFactory as its type\\.$#" - count: 2 - path: SearchGraphQl/Cron/SyncChangedProducts.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\App\\\\RequestInterface\\:\\:getHeader\\(\\)\\.$#" - count: 1 - path: SearchGraphQl/Model/Api/Auth/ApiKeyValidator.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\App\\\\RequestInterface\\:\\:getServer\\(\\)\\.$#" - count: 1 - path: SearchGraphQl/Model/Api/Auth/ApiKeyValidator.php - - - - message: "#^Parameter \\#1 \\$name of method Magento\\\\Framework\\\\HTTP\\\\Client\\\\Curl\\:\\:setOption\\(\\) expects string, int given\\.$#" - count: 2 - path: SearchGraphQl/Model/Api/Client.php - - - - message: "#^Call to an undefined method Magento\\\\Store\\\\Api\\\\Data\\\\StoreInterface\\:\\:getCurrentCurrencyCode\\(\\)\\.$#" - count: 1 - path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php - - - - message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMaximalFinalPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" - count: 1 - path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php - - - - message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMaximalRegularPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" - count: 1 - path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php - - - - message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMinimalFinalPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" - count: 1 - path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php - - - - message: "#^Parameter \\#1 \\$product of method Magento\\\\CatalogGraphQl\\\\Model\\\\Resolver\\\\Product\\\\Price\\\\ProviderInterface\\:\\:getMinimalRegularPrice\\(\\) expects Magento\\\\Framework\\\\Pricing\\\\SaleableInterface, Magento\\\\Catalog\\\\Api\\\\Data\\\\ProductInterface given\\.$#" - count: 1 - path: SearchGraphQl/Model/Price/VanillaMagentoPriceCalculator.php - - - - message: "#^Call to method create\\(\\) on an unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" - count: 1 - path: SearchGraphQl/Model/Resolver/BradProducts.php - - - - message: "#^Comparison operation \"\\>\" between int\\<1, 300\\> and 0 is always true\\.$#" - count: 1 - path: SearchGraphQl/Model/Resolver/BradProducts.php - - - - message: "#^Else branch is unreachable because ternary operator condition is always true\\.$#" - count: 1 - path: SearchGraphQl/Model/Resolver/BradProducts.php - - - - message: "#^Parameter \\$collectionFactory of method BradSearch\\\\SearchGraphQl\\\\Model\\\\Resolver\\\\BradProducts\\:\\:__construct\\(\\) has invalid type Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory\\.$#" - count: 2 - path: SearchGraphQl/Model/Resolver/BradProducts.php - - - - message: "#^Property BradSearch\\\\SearchGraphQl\\\\Model\\\\Resolver\\\\BradProducts\\:\\:\\$collectionFactory has unknown class Magento\\\\Catalog\\\\Model\\\\ResourceModel\\\\Product\\\\CollectionFactory as its type\\.$#" - count: 2 - path: SearchGraphQl/Model/Resolver/BradProducts.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" - count: 1 - path: SearchGraphQl/Model/Resolver/SortPopularityResolver.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" - count: 1 - path: SearchGraphQl/Model/Resolver/SortPopularitySalesResolver.php - - - - message: "#^Call to an undefined method Magento\\\\Framework\\\\GraphQl\\\\Query\\\\Resolver\\\\ContextInterface\\:\\:getExtensionAttributes\\(\\)\\.$#" - count: 1 - path: SearchGraphQl/Model/Resolver/UpdateConfig.php - - - - message: "#^Parameter \\#1 \\$name of method Magento\\\\Framework\\\\HTTP\\\\Client\\\\Curl\\:\\:setOption\\(\\) expects string, int given\\.$#" - count: 3 - path: SearchGraphQl/Model/Sync/WebhookNotifier.php diff --git a/phpstan.neon.dist b/phpstan.neon.dist deleted file mode 100644 index cef647e..0000000 --- a/phpstan.neon.dist +++ /dev/null @@ -1,21 +0,0 @@ -includes: - - phpstan-baseline.neon - -parameters: - level: 5 - paths: - - Analytics - - Autocomplete - - ProductFeatures - - SearchGraphQl - excludePaths: - - '*/Test/*' - - '*/registration.php' - - vendor/* - treatPhpDocTypesAsCertain: false - parallel: - maximumNumberOfProcesses: 4 - processTimeout: 300.0 - # Analyze as PHP 8.3 so 8.4 "implicitly nullable" deprecations don't fire; - # CI matrix still covers real 8.4 runs. - phpVersion: 80300