Skip to content

Commit 6197587

Browse files
authored
[CI] Add PHPStan to CI (#1075)
## Why? We are migrating all Sylius repositories from Psalm to PHPStan for better static analysis consistency across the ecosystem. ## What was done? - Replaced Psalm with PHPStan in CI workflow - Set PHPStan to **max level** with a baseline to track existing issues - Generated baseline file containing 561 existing issues to prevent regression while allowing gradual improvement - Configured comprehensive ignore rules for optional dependencies (MongoDB ODM, PHPCR, deprecated packages) - Fixed immediate issues: Removed unnecessary ternary operator in RequestConfiguration - Updated build configuration: Added baseline to Docker build and package export ignores ## Technical details - PHPStan runs at maximum strictness level to catch all possible issues - Baseline ensures CI passes while documenting technical debt - Configuration handles multiple Symfony versions (6.4 and 7.x) compatibility - Properly excludes test files, specs, and optional dependency code from analysis
2 parents 3d972c8 + e48fb7c commit 6197587

File tree

7 files changed

+2813
-101
lines changed

7 files changed

+2813
-101
lines changed

.dockerignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
!/ecs.php
66
!/phpspec.yml.dist
77
!/phpstan.neon
8+
!/phpstan-baseline.neon
89
!/phpunit.xml.dist
910
!/psalm.xml
1011
!/rector.php

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/Makefile export-ignore
1010
/phpspec.yml.dist export-ignore
1111
/phpstan.neon export-ignore
12+
/phpstan-baseline.neon export-ignore
1213
/phpunit.xml.dist export-ignore
1314
/psalm.xml export-ignore
1415
/rector.php

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ jobs:
7575
(cd tests/Application && bin/console doctrine:schema:create)
7676
7777
-
78-
name: Run Psalm
79-
run: vendor/bin/psalm --php-version=${{ matrix.php }}
78+
name: Run PHPStan
79+
run: vendor/bin/phpstan analyse --ansi --memory-limit=-1 -c phpstan.neon src
8080

8181
-
8282
name: Run analysis

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@
138138
"analyse": [
139139
"@composer validate --strict",
140140
"vendor/bin/ecs check",
141-
"vendor/bin/phpstan analyse --ansi --memory-limit=256M -c phpstan.neon -l max src"
141+
"vendor/bin/phpstan analyse --ansi --memory-limit=256M -c phpstan.neon src"
142142
],
143143
"fix": [
144144
"vendor/bin/ecs check --fix"

phpstan-baseline.neon

Lines changed: 2711 additions & 0 deletions
Large diffs are not rendered by default.

phpstan.neon

Lines changed: 96 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,111 @@
11
includes:
2+
- phpstan-baseline.neon
23
- vendor/phpstan/phpstan-webmozart-assert/extension.neon
34
- vendor/phpstan/phpstan-phpunit/extension.neon
45

56
- vendor/phpstan/phpstan-phpunit/rules.neon
67

78
parameters:
9+
level: max
10+
811
reportUnmatchedIgnoredErrors: false
912

13+
paths:
14+
- 'src/'
15+
1016
excludePaths:
11-
- %currentWorkingDirectory%/src/Bundle/Controller/*
12-
- %currentWorkingDirectory%/src/Bundle/DependencyInjection/Configuration.php
13-
- %currentWorkingDirectory%/src/Bundle/DependencyInjection/PagerfantaConfiguration.php
14-
- %currentWorkingDirectory%/src/Bundle/DependencyInjection/Driver/Doctrine/DoctrineODMDriver.php
15-
- %currentWorkingDirectory%/src/Bundle/DependencyInjection/Driver/Doctrine/DoctrinePHPCRDriver.php
16-
- %currentWorkingDirectory%/src/Bundle/Doctrine/ODM/*
17-
- %currentWorkingDirectory%/src/Bundle/EventListener/ODM*
18-
- %currentWorkingDirectory%/src/Bundle/Event/ResourceControllerEvent.php
19-
- %currentWorkingDirectory%/src/Bundle/Form/Extension/HttpFoundation/HttpFoundationRequestHandler.php
20-
- %currentWorkingDirectory%/src/Bundle/Routing/ResourceLoader.php
21-
- %currentWorkingDirectory%/src/Bundle/Routing/Configuration.php
22-
- %currentWorkingDirectory%/src/Bundle/spec/*
23-
- %currentWorkingDirectory%/src/Bundle/Tests/*
24-
- %currentWorkingDirectory%/src/Component/legacy/spec/*
25-
- %currentWorkingDirectory%/src/Component/legacy/src/Annotation/*
26-
- %currentWorkingDirectory%/src/Component/legacy/src/Exception/*
27-
- %currentWorkingDirectory%/src/Component/legacy/src/Factory/*
28-
- %currentWorkingDirectory%/src/Component/legacy/src/Generator/*
29-
- %currentWorkingDirectory%/src/Component/legacy/src/Metadata/*
30-
- %currentWorkingDirectory%/src/Component/legacy/src/Model/*
31-
- %currentWorkingDirectory%/src/Component/legacy/src/Reflection/ClassReflection.php
32-
- %currentWorkingDirectory%/src/Component/legacy/src/Repository/*
33-
- %currentWorkingDirectory%/src/Component/legacy/src/StateMachine/*
34-
- %currentWorkingDirectory%/src/Component/legacy/src/Storage/*
35-
- %currentWorkingDirectory%/src/Component/legacy/src/ResourceActions.php
36-
- %currentWorkingDirectory%/src/Component/legacy/src/Translation/*
37-
- %currentWorkingDirectory%/src/Component/legacy/tests/*
38-
- %currentWorkingDirectory%/src/Component/spec/*
39-
- %currentWorkingDirectory%/src/Component/tests/*
40-
- %currentWorkingDirectory%/src/Component/vendor/*
17+
# External vendor dependencies installed via composer in Component
18+
# These are third-party packages that shouldn't be analyzed
19+
- 'src/Component/vendor/'
20+
21+
# PHPSpec tests use a different syntax and structure than regular PHP code
22+
# They contain specs with methods like it_*() and shouldReturn() that PHPStan doesn't understand
23+
- 'src/Bundle/spec/'
24+
- 'src/Component/spec/'
25+
26+
# PHPUnit tests - test code is not production code and has different quality requirements
27+
# Tests often use mocks, stubs and test-specific patterns
28+
- 'src/Bundle/Tests/'
29+
- 'src/Component/tests/'
30+
31+
# MongoDB ODM integration - requires doctrine/mongodb-odm which is optional
32+
# These files contain references to MongoDB classes that don't exist without the package
33+
- 'src/Bundle/Doctrine/ODM/'
34+
- 'src/Bundle/EventListener/ODM*'
35+
- 'src/Bundle/DependencyInjection/Driver/Doctrine/DoctrineODMDriver.php'
36+
37+
# Legacy code maintained for backward compatibility
38+
# Contains deprecated interfaces and implementations that will be removed in 2.0
39+
- 'src/Component/legacy/'
40+
41+
# Parameters.php extends Symfony's ParameterBag but changes return type covariance
42+
# This is a known limitation that cannot be fixed without breaking BC
43+
# PHPStan reports: "Return type mixed of method Parameters::get() is not covariant with
44+
# return type mixed of method ParameterBag::get()"
45+
- 'src/Bundle/Controller/Parameters.php'
4146

4247
ignoreErrors:
43-
- '/Call to method type\(\) on an unknown class Doctrine\\ORM\\Mapping\\AssociationMapping./'
44-
- '/Call to method getArguments\(\) on an unknown class ReflectionAttribute./'
45-
- '/Call to method isChangeTrackingDeferredExplicit\(\) on an unknown class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata./'
46-
- '/Call to an undefined method ReflectionClass::getAttributes\(\)./'
47-
- '/Call to an undefined method object::getRealClassName\(\)./'
48-
- '/Class Bazinga\\Bundle\\HateoasBundle\\BazingaHateoasBundle not found\./'
49-
- '/Class Doctrine\\ODM\\MongoDB\\DocumentManager not found\./'
50-
- '/Class Doctrine\\ODM\\PHPCR\\Document\\Resource not found\./'
51-
- '/Class Doctrine\\Bundle\\MongoDBBundle/'
52-
- '/Class Doctrine\\Bundle\\PHPCRBundle/'
53-
- '/Class Doctrine\\Common\\Persistence\\ObjectManager not found\./'
54-
- '/Class Doctrine\\ODM\\MongoDB\\Mapping\\ClassMetadata not found\./'
48+
# Optional MongoDB ODM support - requires doctrine/mongodb-odm-bundle package
49+
# These classes are only loaded when MongoDB driver is configured and the bundle is installed
50+
# See: src/Bundle/AbstractResourceBundle.php:112
51+
- '/Class Doctrine\\Bundle\\MongoDBBundle\\DependencyInjection\\Compiler\\DoctrineMongoDBMappingsPass not found/'
52+
- '/Class Doctrine\\ODM\\MongoDB\\DocumentManager not found/'
53+
- '/has invalid type Doctrine\\MongoDB\\Query\\Builder/'
54+
55+
# Optional PHPCR ODM support - requires doctrine/phpcr-bundle package
56+
# PHPCR is deprecated since 1.3 and will be removed in 2.0
57+
# See: src/Bundle/AbstractResourceBundle.php:120-127
58+
- '/Class Doctrine\\Bundle\\PHPCRBundle\\DependencyInjection\\Compiler\\DoctrinePhpcrMappingsPass not found/'
59+
- '/Class Doctrine\\ODM\\PHPCR\\Document\\Resource not found/'
60+
- '/has invalid type Doctrine\\ODM\\PHPCR\\Query\\Builder\\QueryBuilder/'
61+
- '/has invalid type Doctrine\\ODM\\PHPCR\\DocumentManagerInterface/'
62+
63+
# Backward compatibility with Doctrine Common 2.x
64+
# Namespace moved from Doctrine\Common\Persistence to Doctrine\Persistence in 3.x
65+
# Code uses both namespaces with aliases to support both versions
66+
# See: src/Bundle/DependencyInjection/Driver/Doctrine/AbstractDoctrineDriver.php:46
67+
- '/Class Doctrine\\Common\\Persistence\\ObjectManager not found/'
68+
69+
# Doctrine ORM 3.x introduces new mapping classes
70+
# AssociationMapping class exists only in ORM 3.x, not in ORM 2.x
71+
# Code supports both versions, so we need to ignore these errors when running with ORM 2.x
72+
# See: src/Bundle/EventListener/ORMMappedSuperClassSubscriber.php
73+
- '/Class Doctrine\\ORM\\Mapping\\AssociationMapping not found/'
74+
- '/Call to method .* on an unknown class Doctrine\\ORM\\Mapping\\AssociationMapping/'
75+
- '/PHPDoc tag @var for variable .* contains unknown class Doctrine\\ORM\\Mapping\\AssociationMapping/'
76+
77+
# Optional symfony/web-link component - only used when installed
78+
# Code checks class_exists() before using these classes
79+
# See: src/Bundle/Controller/ControllerTrait.php:443-448
80+
- '/has invalid type Psr\\Link\\LinkInterface/'
81+
- '/Class Symfony\\Component\\WebLink\\GenericLinkProvider not found/'
82+
- '/Instantiated class Symfony\\Component\\WebLink\\GenericLinkProvider not found/'
83+
84+
# Backward compatibility with deprecated Symfony ExpressionLanguage cache interfaces
85+
# ParserCacheInterface was removed in Symfony 4.0, replaced by PSR-6 CacheItemPoolInterface
86+
# Code provides BC layer with trigger_deprecation
87+
# See: src/Bundle/ExpressionLanguage/ExpressionLanguage.php:29-40
5588
- '/Class Symfony\\Component\\ExpressionLanguage\\ParserCache\\ParserCacheInterface not found/'
5689
- '/Instantiated class Symfony\\Component\\ExpressionLanguage\\ParserCache\\ParserCacheAdapter not found/'
57-
- '/Method Symfony\\Component\\DependencyInjection\\Alias::setDeprecated\(\)/'
58-
- '/Method Sylius\\Bundle\\ResourceBundle\\Controller\\ResourcesCollectionProvider::get\(\) has no return typehint specified./'
59-
- '/Method Sylius\\Bundle\\ResourceBundle\\Controller\\ResourcesCollectionProviderInterface::get\(\) has no return typehint specified./'
60-
- '/Method Sylius\\Bundle\\ResourceBundle\\Controller\\ResourcesResolver::getResources\(\) has no return typehint specified./'
61-
- '/Method Sylius\\Bundle\\ResourceBundle\\Controller\\ResourcesResolverInterface::getResources\(\) has no return typehint specified./'
62-
- '/Method Sylius\\Bundle\\ResourceBundle\\Event\\ResourceControllerEvent::stop\(\) has no return typehint specified./'
63-
- '/Method Sylius\\Bundle\\ResourceBundle\\Form\\Extension\\HttpFoundation\\HttpFoundationRequestHandler::handleRequest\(\) has no return typehint specified./'
64-
- '/Method Sylius\\Bundle\\ResourceBundle\\Grid\\Controller\\ResourcesResolver::getResources\(\) has no return typehint specified./'
65-
- '/Method Sylius\\Resource\\Metadata\\Extractor\\AbstractResourceExtractor::getResources\(\) should return array<Sylius\\Resource\\Metadata\\ResourceMetadata> but returns array<Sylius\\Resource\\Metadata\\ResourceMetadata>\|null./'
66-
- '/Method Sylius\\Resource\\Model\\ResourceInterface::getId\(\) has no return typehint specified./'
67-
- '/Method Sylius\\Resource\\Model\\TimestampableInterface::setCreatedAt\(\) has no return typehint specified./'
68-
- '/Method Sylius\\Resource\\Model\\TimestampableInterface::setUpdatedAt\(\) has no return typehint specified./'
69-
- '/Method Sylius\\Resource\\Doctrine\\Persistence\\InMemoryRepository::findBy\(\) has parameter \$limit with no type specified./'
70-
- '/Method Sylius\\Resource\\Doctrine\\Persistence\\InMemoryRepository::findBy\(\) has parameter \$offset with no type specified./'
71-
- '/Method Sylius\\Resource\\Symfony\\Controller\\MainController::__invoke\(\) should return Symfony\\Component\\HttpFoundation\\Response but returns mixed./'
72-
- '/Method Symfony\\Component\\Routing\\RouteCollection::add\(\) invoked with 3 parameters, 2 required\./'
73-
- '/Method Symfony\\Contracts\\EventDispatcher\\EventDispatcherInterface::dispatch\(\) invoked with 2 parameters, 1 required\./'
74-
- '/Parameter \#1 \$currentPage of method Pagerfanta\\Pagerfanta<mixed>::setCurrentPage\(\) expects int<1, max>, int given\./'
75-
- '/Parameter \#1 \$maxPerPage of method Pagerfanta\\Pagerfanta<mixed>::setMaxPerPage\(\) expects int<1, max>, int given\./'
76-
- '/Parameter \#1 \$array[0-9]? of function array_multisort expects array, array\|int given\./'
77-
- '/Parameter \#2 \$class of static method Webmozart\\Assert\\Assert::isInstanceOf\(\) expects class-string<object>, string given./'
78-
- '/Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class-string<object>\|object, object\|string given./'
79-
- '/Parameter \#1 \$package of method Symfony\\Component\\DependencyInjection\\Alias::setDeprecated\(\)/'
80-
- '/PHPDoc tag @var for variable \$value contains unknown class Doctrine\\ORM\\Mapping\\AssociationMapping./'
81-
- '/Return typehint of method Sylius\\Bundle\\ResourceBundle\\Routing\\CrudRoutesAttributesLoader::getClassAttributes\(\) has invalid type ReflectionAttribute./'
82-
- '/Return typehint of method Sylius\\Bundle\\ResourceBundle\\Routing\\RoutesAttributesLoader::getClassAttributes\(\) has invalid type ReflectionAttribute./'
83-
- '/Unable to resolve the template type ExpectedType in call to method static method Webmozart\\Assert\\Assert::isInstanceOf\(\)/'
84-
- '/is deprecated since Sylius 1\.8/'
85-
- '/Method Sylius\\Bundle\\ResourceBundle\\Event\\ResourceControllerEvent\:\:stop\(\) has no return type specified\./'
86-
- '/Trying to invoke mixed/'
87-
- '/Method Sylius\\Bundle\\ResourceBundle\\Grid\\Controller\\ResourcesResolver\:\:getResources\(\) has no return type specified\./'
88-
- '/Parameter \#2 \$callback of function preg_replace_callback expects callable\(array\<int\|string\, string\>\): string, Closure\(array\)\: mixed given\./'
89-
- '/Parameter \#1 \$objectOrArray of method Symfony\\Component\\PropertyAccess\\PropertyAccessorInterface\:\:getValue\(\) expects array\|object, array\|object\|null given\./'
90-
- '/Parameter \#1 \$min \(0\) of function random_int expects lower number than parameter \#2 \$max \(int\<\-1, max\>\)\./'
91-
- '/Method Sylius\\Resource\\Model\\ResourceInterface\:\:getId\(\) has no return type specified\./'
92-
- '/Method Sylius\\Resource\\Model\\TimestampableInterface\:\:setCreatedAt\(\) has no return type specified\./'
93-
- '/Method Sylius\\Resource\\Model\\TimestampableInterface\:\:setUpdatedAt\(\) has no return type specified\./'
94-
- '/Method Sylius\\Component\\Resource\\Repository\\InMemoryRepository\:\:findAll\(\) should return array\<object\> but returns array\./'
95-
- '/Method Sylius\\Resource\\State\\Provider\\DeserializeProvider\:\:provide\(\) should return array\|object\|null but returns mixed./'
96-
- '/Method Sylius\\Resource\\Symfony\\Controller\\MainController::__invoke\(\) should return Symfony\\Component\\HttpFoundation\\Response but returns mixed./'
97-
- '/Method Sylius\\Resource\\Symfony\\Serializer\\State\\DeserializeProvider\:\:provide\(\) should return array\|object\|null but returns mixed./'
98-
- '/Method Sylius\\Resource\\Symfony\\EventDispatcher\\GenericEvent\:\:stop\(\) has no return type specified./'
99-
- '/Method Sylius\\Bundle\\ResourceBundle\\Form\\Extension\\HttpFoundation\\HttpFoundationRequestHandler::handleRequest\(\) has no return type specified./'
100-
- '/Method Sylius\\Bundle\\ResourceBundle\\Grid\\Controller\\ResourcesResolver::getResources\(\) has no return type specified./'
101-
- '/Method Sylius\\Component\\Resource\\Model\\ResourceInterface::getId\(\) has no return type specified./'
102-
- '/Method Sylius\\Component\\Resource\\Model\\TimestampableInterface::set(Created|Updated)At\(\) has no return type specified./'
103-
- '/Parameter #1 \$objectOrArray of method Symfony\\Component\\PropertyAccess\\PropertyAccessor::getValue\(\) expects array\|object, mixed given\./'
104-
- '/Parameter #1 \$objectOrArray of method Symfony\\Component\\PropertyAccess\\PropertyAccessorInterface::getValue\(\) expects array\|object, mixed given\./'
105-
- '/Parameter #4 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, mixed given\./'
106-
- '/Parameter #1 \$submittedData of method Symfony\\Component\\Form\\FormInterface::submit\(\) expects array\|string\|null, mixed given\./'
107-
- '/Parameter #2 \$callback of function preg_replace_callback expects callable\(array<int\|string, string>\): string, Closure\(array\): mixed given\./'
108-
- '/Unable to resolve the template type T in call to method Doctrine\\Persistence\\ObjectManager::getClassMetadata\(\)/'
109-
-
110-
identifier: missingType.generics
90+
91+
# Symfony Config Component uses fluent interface with dynamic return types
92+
# getRootNode() returns NodeDefinition which gets transformed to ArrayNodeDefinition
93+
# when children() is called. PHPStan cannot track these runtime type transformations
94+
# See: src/Bundle/DependencyInjection/Configuration.php:30-37
95+
- '/Call to an undefined method Symfony\\Component\\Config\\Definition\\Builder\\NodeParentInterface::(end|variableNode|scalarNode)\(\)/'
96+
97+
# Backward compatibility for Symfony DependencyInjection Alias::setDeprecated()
98+
# Method signature changed between Symfony versions:
99+
# - Symfony < 5.1: setDeprecated($status, $message)
100+
# - Symfony >= 5.1: setDeprecated($package, $version, $message)
101+
# Code dynamically detects parameter count and calls appropriately
102+
# See: src/Bundle/DependencyInjection/Compiler/PagerfantaBridgePass.php:62-73
103+
- '/Method Symfony\\Component\\DependencyInjection\\Alias::setDeprecated\(\) invoked with 2 parameters, 3 required/'
104+
- '/Parameter #1 \$package of method Symfony\\Component\\DependencyInjection\\Alias::setDeprecated\(\) expects string, true given/'
105+
106+
# ResourceControllerEvent uses if(false) for IDE support - provides class alias hint
107+
# This is intentional dead code to help IDEs understand the class_alias in GenericEvent.php
108+
# See: src/Bundle/Event/ResourceControllerEvent.php:18-22
111109
-
112-
identifier: missingType.iterableValue
110+
message: '#If condition is always false#'
111+
path: src/Bundle/Event/ResourceControllerEvent.php

src/Bundle/Controller/RequestConfiguration.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public function getDefaultTemplate($name)
8383
$templatesNamespace = (string) $this->metadata->getTemplatesNamespace();
8484

8585
if (false !== strpos($templatesNamespace, ':')) {
86-
return sprintf('%s:%s.%s', $templatesNamespace ?: ':', $name, 'twig');
86+
return sprintf('%s:%s.%s', $templatesNamespace, $name, 'twig');
8787
}
8888

8989
return sprintf('%s/%s.%s', $templatesNamespace, $name, 'twig');

0 commit comments

Comments
 (0)