Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9b51d56
Update docs
shochdoerfer Sep 26, 2026
c502190
Remove dead ignoreErrors and reportUnmatchedIgnoredErrors from phpsta…
shochdoerfer Sep 26, 2026
96873dc
Raise grid-bundle dev floor from ^1.13 to ^1.15
shochdoerfer Sep 26, 2026
466b100
Add explicit grid-bundle <1.16 and >=1.16 lanes to CI
shochdoerfer Sep 26, 2026
b00b24b
Stop resource attribute rules from crashing the analysis
shochdoerfer Sep 26, 2026
43591c6
Check grid resource class on InClassNode, attribute before hierarchy
shochdoerfer Sep 26, 2026
6b5dea5
Fix grid field rule leaking resource class across fields
shochdoerfer Sep 26, 2026
17542ae
Report each resolved version in its own composer show call
shochdoerfer Sep 26, 2026
8bd3986
Make the generic filter node match filters again
shochdoerfer Sep 26, 2026
01d0aff
Collect grid fields built with CallableField::createForService()
shochdoerfer Sep 26, 2026
bff8e2b
Keep the test filter registry in the order extension.neon uses
shochdoerfer Sep 26, 2026
8b3184c
Add filter nodes for BooleanFilter, DateFilter and MoneyFilter
shochdoerfer Sep 26, 2026
081a305
Bring AGENTS.md back in line with the code
shochdoerfer Sep 26, 2026
d73d56a
Correct the @implements annotation on both grid rules
shochdoerfer Sep 26, 2026
dc1f237
Record the instanceof/@implements trap that the guard was hiding
shochdoerfer Sep 26, 2026
9f83599
Narrow every getNodeType() return to its concrete node type
shochdoerfer Sep 26, 2026
5159bcc
Cover filter registry ordering with regression tests
shochdoerfer Sep 26, 2026
71e77d7
Clean up test data used in the unit tests
shochdoerfer Sep 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,22 @@ on:

jobs:
run:
name: php ${{ matrix.php-versions }} / grid-bundle ${{ matrix.grid-bundle }}
runs-on: ${{ matrix.operating-system }}
strategy:
fail-fast: false
matrix:
operating-system: ['ubuntu-latest']
php-versions: ['8.2']
# The extension supports grid-bundle >= 1.15. Sylius 1.16 moved the
# builder interfaces into Sylius\Component\Grid and deprecated the
# Sylius\Bundle\GridBundle equivalents, so both sides of that boundary
# need explicit coverage. Constraints are pinned per lane instead of
# reusing the committed lockfile, otherwise bumping the lock to 1.16
# would silently turn the low lane into a second high lane.
grid-bundle:
- '^1.15 <1.16'
- '^1.16'
coveralls: [ false ]
steps:
- name: Checkout repo
Expand All @@ -26,7 +37,15 @@ jobs:
extensions: bcmath, gd

- name: Install Composer dependencies
run: composer install
run: |
composer require --dev "sylius/grid-bundle:${{ matrix.grid-bundle }}" --no-update --no-interaction
composer update --no-interaction --no-progress

- name: Show resolved dependency versions
run: |
composer show sylius/grid-bundle
composer show phpstan/phpstan
composer show phpunit/phpunit

- name: Composer license check
run: composer check-license
Expand Down
11 changes: 10 additions & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,16 @@

$finder = (new PhpCsFixer\Finder())
->in(__DIR__)
->exclude(['vendor']);
->exclude([
'vendor',
// Analysis fixtures are loaded through composer autoload-dev.files, not
// by class name, so the psr_autoloading rule that @Symfony enables does
// not apply to them: their file name is snake_case while their classes
// are PSR-1. Without this the fixer would rewrite every fixture class to
// its file's basename, which breaks the references between fixtures.
'tests/bitExpert/PHPStan/Sylius/Rule/Grid/data',
'tests/bitExpert/PHPStan/Sylius/Rule/Resource/data',
]);

return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
Expand Down
624 changes: 624 additions & 0 deletions AGENTS.md

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,12 @@ This PHPStan extension works for both Sylius plugins and Sylius application proj
The following rules have been implemented:
- Rule to check if resource classes defined either via AbstractGrid::getResourceClass() or the #AsGrid attribute exist
- Rule to check that configured grid fields belong to the configured resource class
- - custom field types are supported
- Rule to check that configured filter fields belong to the configured resource class
- custom filter types are supported
- Rule to check that grid class configured via the `Index` attribute exists
- Rule to check that form type configured via the `AsResource` attribute exists

Current assumptions:
- Grids are configured by extending the `Sylius\Bundle\GridBundle\Grid\AbstractGrid` class

### Custom filter types

To include your custom filter type in the checks, implement the `bitExpert\PHPStan\Sylius\Collector\Grid\Filter\FilterNode` interface and add a service to your `phpstan.neon` file and tag it with the `phpstan.sylius.grid.filter` tag.
Expand Down
10 changes: 8 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
},
"require-dev": {
"sylius/resource-bundle": "^1.12",
"sylius/grid-bundle": "^1.13",
"sylius/grid-bundle": "^1.15",
"nikic/php-parser": "^5.4",
"phpunit/phpunit": "^11.5",
"friendsofphp/php-cs-fixer": "^3.69",
Expand All @@ -34,9 +34,15 @@
"files": [
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/entity.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/custom_filter.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid_custom_filter.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid_needs_resource_model.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid_needs_resource_model_attr.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity.php"
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid_needs_resource_model_no_methods.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid_needs_resource_model_native_interface.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity_index.php",
"tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity_non_constant_attribute.php"
]
},
"extra": {
Expand Down
2 changes: 1 addition & 1 deletion composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 17 additions & 2 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -66,11 +66,19 @@ services:
tags:
- phpstan.sylius.grid.filter
-
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\ExistsFilter
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\BooleanFilter
tags:
- phpstan.sylius.grid.filter
-
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\Filter
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\DateFilter
tags:
- phpstan.sylius.grid.filter
-
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\MoneyFilter
tags:
- phpstan.sylius.grid.filter
-
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\ExistsFilter
tags:
- phpstan.sylius.grid.filter
-
Expand All @@ -81,3 +89,10 @@ services:
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\StringFilter
tags:
- phpstan.sylius.grid.filter

# Registered last on purpose: this node matches every filter that
# implements FilterInterface, so all concrete nodes have to be asked first.
-
class: bitExpert\PHPStan\Sylius\Collector\Grid\Filter\Filter
tags:
- phpstan.sylius.grid.filter
18 changes: 7 additions & 11 deletions phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
parameters:
level: max
reportUnmatchedIgnoredErrors: false
treatPhpDocTypesAsCertain: false
paths:
- src
- tests
excludePaths:
- tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/entity.php
- tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid.php
- tests/bitExpert/PHPStan/Sylius/Rule/Grid/data/grid_custom_filter.php
- tests/bitExpert/PHPStan/Sylius/Rule/Grid//data/grid_needs_resource_model.php
- tests/bitExpert/PHPStan/Sylius/Rule/Grid//data/grid_needs_resource_model_attr.php
- tests/bitExpert/PHPStan/Sylius/Rule/Grid//data/grid_needs_resource_model_no_methods.php
# Only parses on grid-bundle >= 1.16, so it must not be statically analysed
# against the >= 1.15 lane.
- tests/bitExpert/PHPStan/Sylius/Rule/Grid//data/grid_needs_resource_model_native_interface.php
- tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity.php
- tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity_index.php
- tests/bitExpert/PHPStan/Sylius/Rule/Resource/data/entity_non_constant_attribute.php
fileExtensions:
- php
ignoreErrors:
-
message: '~Cannot cast mixed to string.~'
path: src/bitExpert/PHPStan/Sylius/Rule/Grid/AbstractGridBuilderRule.php
-
message: '~Cannot call~'
path: src/bitExpert/PHPStan/Sylius/Rule/Grid/GridBuilderFieldIsPartOfResourceClass.php
-
message: '~Cannot call~'
path: src/bitExpert/PHPStan/Sylius/Rule/Grid/GridBuilderFilterIsPartOfResourceClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@
*/
final class CollectFieldsForGridClass extends AbstractGridClassCollector implements Collector
{
/**
* Grid fields are built through a static factory. Every field class uses create(),
* and CallableField additionally offers createForService() for a service-backed
* callable. Both take the field name as their first argument.
*/
private const FACTORY_METHODS = ['create', 'createForService'];

public function __construct(private readonly FieldRegistry $fieldRegistry)
{
}
Expand All @@ -45,7 +52,7 @@ public function processNode(Node $node, Scope $scope): ?array
return null;
}

if ((!$node->name instanceof Identifier) || ('create' !== $node->name->toString())) {
if (!$node->name instanceof Identifier || !\in_array($node->name->toString(), self::FACTORY_METHODS, true)) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the phpstan-sylius package.
*
* (c) bitExpert AG
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);

namespace bitExpert\PHPStan\Sylius\Collector\Grid\Filter;

use bitExpert\PHPStan\Util\PropertyName;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;

final readonly class BooleanFilter implements FilterNode
{
private const FILTER_TYPE = 'Sylius\\Bundle\\GridBundle\\Builder\\Filter\\BooleanFilter';

public function supports(FullyQualified $nodeClass): bool
{
return self::FILTER_TYPE === $nodeClass->name;
}

public function getFilterFields(StaticCall $node): array
{
$filterFields = [];

if (isset($node->args[0])) {
$arg = $node->args[0];
if ($arg instanceof Arg && $arg->value instanceof String_) {
$filterFields[] = PropertyName::convertSnakeToCamelCase($arg->value->value);
}
}

return $filterFields;
}
}
43 changes: 43 additions & 0 deletions src/bitExpert/PHPStan/Sylius/Collector/Grid/Filter/DateFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

/*
* This file is part of the phpstan-sylius package.
*
* (c) bitExpert AG
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);

namespace bitExpert\PHPStan\Sylius\Collector\Grid\Filter;

use bitExpert\PHPStan\Util\PropertyName;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;

final readonly class DateFilter implements FilterNode
{
private const FILTER_TYPE = 'Sylius\\Bundle\\GridBundle\\Builder\\Filter\\DateFilter';

public function supports(FullyQualified $nodeClass): bool
{
return self::FILTER_TYPE === $nodeClass->name;
}

public function getFilterFields(StaticCall $node): array
{
$filterFields = [];

if (isset($node->args[0])) {
$arg = $node->args[0];
if ($arg instanceof Arg && $arg->value instanceof String_) {
$filterFields[] = PropertyName::convertSnakeToCamelCase($arg->value->value);
}
}

return $filterFields;
}
}
17 changes: 14 additions & 3 deletions src/bitExpert/PHPStan/Sylius/Collector/Grid/Filter/Filter.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,26 @@

final readonly class Filter implements FilterNode
{
private const FILTER_TYPE = 'Sylius\\Bundle\\GridBundle\\Builder\\Filter\\FilterInterface';
/**
* Both the old bundle interface and the one introduced in 1.16. The concrete
* grid-bundle filter factories return the new interface from 1.16 on, so
* checking only the old one would make this node silently stop matching.
*/
private const FILTER_TYPES = [
'Sylius\\Bundle\\GridBundle\\Builder\\Filter\\FilterInterface',
'Sylius\\Component\\Grid\\Builder\\Filter\\FilterInterface',
];

public function supports(FullyQualified $nodeClass): bool
{
try {
$filterType = new ObjectType(self::FILTER_TYPE);
$nodeClassType = new ObjectType($nodeClass->toString());

return $nodeClassType->isSuperTypeOf($filterType)->yes();
foreach (self::FILTER_TYPES as $filterType) {
if ((new ObjectType($filterType))->isSuperTypeOf($nodeClassType)->yes()) {
return true;
}
}
} catch (\Throwable $e) {
}

Expand Down
45 changes: 45 additions & 0 deletions src/bitExpert/PHPStan/Sylius/Collector/Grid/Filter/MoneyFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/*
* This file is part of the phpstan-sylius package.
*
* (c) bitExpert AG
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);

namespace bitExpert\PHPStan\Sylius\Collector\Grid\Filter;

use bitExpert\PHPStan\Util\PropertyName;
use PhpParser\Node\Arg;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;

final readonly class MoneyFilter implements FilterNode
{
private const FILTER_TYPE = 'Sylius\\Bundle\\GridBundle\\Builder\\Filter\\MoneyFilter';

public function supports(FullyQualified $nodeClass): bool
{
return self::FILTER_TYPE === $nodeClass->name;
}

public function getFilterFields(StaticCall $node): array
{
$filterFields = [];

// create(string $name, string $currencyCode, ?int $scale = null):
// the currency code is the second argument, the field is the first one.
if (isset($node->args[0])) {
$arg = $node->args[0];
if ($arg instanceof Arg && $arg->value instanceof String_) {
$filterFields[] = PropertyName::convertSnakeToCamelCase($arg->value->value);
}
}

return $filterFields;
}
}
Loading
Loading