symplify / phpstan-rules
Set of Symplify rules, type extensions and error formatter for PHPStan
Package info
github.com/symplify/phpstan-rules
Type:phpstan-extension
pkg:composer/symplify/phpstan-rules
Requires
- php: ^8.4
- nette/utils: ^4.1
- phpstan/phpdoc-parser: ^2.3.5
- phpstan/phpstan: ^2.2.14
- webmozart/assert: ^2.4
Requires (Dev)
- illuminate/container: ^11.51
- nette/neon: ^3.4
- nikic/php-parser: ^5.9
- phpstan/extension-installer: ^1.4
- phpunit/phpunit: ^13.3
- rector/jack: ^1.1
- rector/rector: ^2.6.7
- shipmonk/composer-dependency-analyser: ^1.8
- symfony/framework-bundle: ^6.4
- symplify/easy-coding-standard: ^13.3
- tomasvotruba/class-leak: ^2.2
- tomasvotruba/type-coverage: ^2.3
- tomasvotruba/unused-public: ^2.2
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 14.15.3
- 14.15.2
- 14.15.1
- 14.15.0
- 14.14.1
- 14.14.0
- 14.13.1
- 14.13.0
- 14.12.4
- 14.12.3
- 14.12.2
- 14.12.1
- 14.12.0
- 14.11.0
- 14.10.0
- 14.9.11
- 14.9.10
- 14.9.9
- 14.9.8
- 14.9.7
- 14.9.6
- 14.9.5
- 14.9.4
- 14.9.3
- 14.9.2
- 14.9.1
- 14.9.0
- 14.8.5
- 14.8.4
- 14.8.3
- 14.8.2
- 14.8.1
- 14.8.0
- 14.7.0
- 14.6.13
- 14.6.12
- 14.6.11
- 14.6.10
- 14.6.9
- 14.6.8
- 14.6.7
- 14.6.6
- 14.6.5
- 14.6.4
- 14.6.3
- 14.6.2
- 14.6.1
- 14.6.0
- 14.5.3
- 14.5.2
- 14.5.1
- 14.5.0
- 14.4.9
- 14.4.8
- 14.4.7
- 14.4.6
- 14.4.5
- 14.4.4
- 14.4.3
- 14.4.2
- 14.4.1
- 14.4.0
- 14.3.5
- 14.3.4
- 14.3.3
- 14.3.2
- 14.3.1
- 14.3.0
- 14.2.8
- 14.2.7
- 14.2.6
- 14.2.5
- 14.2.4
- 14.2.3
- 14.2.2
- 14.2.1
- 14.2.0
- 14.1.5
- 14.1.4
- 14.1.3
- 14.1.2
- 14.1.1
- 14.1.0
- 14.0.4
- 14.0.3
- 14.0.2
- 14.0.1
- 14.0.0
- 13.0.1
- 13.0.0
- 12.7.0
- 12.6.4
- 12.6.3
- 12.6.2
- 12.6.1
- 12.6.0
- 12.5.2
- 12.5.1
- 12.5.0
- 12.4.9
- 12.4.8
- 12.4.7
- 12.4.6
- 12.4.5
- 12.4.4
- 12.4.3
- 12.4.2
- 12.4.1
- 12.4.0
- 12.3.1
- 12.3.0.72
- 12.3.0
- 12.2.7.72
- 12.2.7
- 12.2.6.72
- 12.2.6
- 12.2.5.72
- 12.2.5
- 12.2.4.72
- 12.2.4
- 12.2.3.72
- 12.2.3
- 12.2.2.72
- 12.2.2
- 12.2.1.72
- 12.2.1
- 12.2.0.72
- 12.2.0
- 12.1.4.72
- 12.1.4
- 12.1.3.72
- 12.1.3
- 12.1.2.72
- 12.1.2
- 12.1.1.72
- 12.1.1
- 12.1.0.72
- 12.1.0
- 12.0.4.72
- 12.0.4
- 12.0.3.72
- 12.0.3
- 12.0.2.72
- 12.0.2
- 12.0.1.72
- 12.0.1
- 12.0.0.72
- 12.0.0
- 11.4.1.72
- 11.4.1
- 11.4.0.72
- 11.4.0
- 11.3.5.72
- 11.3.5
- 11.3.4.72
- 11.3.4
- 11.3.3.72
- 11.3.3
- 11.3.2.72
- 11.3.2
- 11.3.1.72
- 11.3.1
- 11.3.0.72
- 11.3.0
- 11.2.5.72
- 11.2.5
- 11.2.4.72
- 11.2.4
- 11.2.3.72
- 11.2.3
- 11.2.2.72
- 11.2.2
- 11.2.1
- 11.2.0.72
- 11.2.0
- 11.1.28.72
- 11.1.28
- 11.1.27.72
- 11.1.27
- 11.1.26.72
- 11.1.26
- 11.1.25.72
- 11.1.25
- 11.1.24
- 11.1.23
- 11.1.22
- 11.1.21
- 11.1.20
- 11.1.19
- 11.1.18
- 11.1.17
- 11.1.16
- 11.1.15
- 11.1.14
- 11.1.13
- 11.1.12
- 11.1.11
- 11.1.10
- 11.1.9
- 11.1.8
- 11.1.7
- 11.1.6
- 11.1.5
- 11.1.4
- 11.1.1
- 11.1.0
- 11.0.9
- 11.0.8
- 11.0.7
- 11.0.6
- 11.0.5
- 11.0.4
- 11.0.3
- 11.0.2
- 11.0.1
- 11.0.0
- 10.3.3
- 10.3.2
- 10.3.1
- 10.3.0
- 10.2.11
- 10.2.10
- 10.2.9
- 10.2.8
- 10.2.7
- 10.2.6
- 10.2.5
- 10.2.4
- 10.2.3
- 10.2.2
- 10.2.1
- 10.2.0
- 10.1.4
- 10.1.3
- 10.1.2
- 10.1.1
- 10.1.0
- 10.0.25
- 10.0.24
- 10.0.23
- 10.0.22
- 10.0.21
- 10.0.20
- 10.0.19
- 10.0.18
- 10.0.17
- 10.0.16
- 10.0.15
- 10.0.14
- 10.0.13
- 10.0.12
- 10.0.11
- 10.0.10
- 10.0.9
- 10.0.8
- 10.0.7
- 10.0.6
- 10.0.5
- 10.0.4
- 10.0.3
- 10.0.2
- 10.0.1
- 10.0.0
- 10.0.0-beta17
- 10.0.0-beta16
- 10.0.0-beta15
- 10.0.0-beta14
- 10.0.0-beta13
- 10.0.0-beta12
- 10.0.0-beta11
- 10.0.0-beta10
- 10.0.0-beta9
- 10.0.0-beta8
- 10.0.0-beta7
- 10.0.0-beta6
- 10.0.0-beta5
- 10.0.0-beta4
- 10.0.0-beta3
- 10.0.0-beta2
- 10.0.0-beta1
- v9.5.0
- 9.4.70
- 9.4.69
- 9.4.68
- 9.4.67
- 9.4.66
- 9.4.65
- 9.4.64
- 9.4.63
- 9.4.62
- 9.4.61
- 9.4.60
- 9.4.59
- 9.4.58
- 9.4.57
- 9.4.56
- 9.4.55
- 9.4.54
- 9.4.53
- 9.4.52
- 9.4.51
- 9.4.50
- 9.4.49
- 9.4.48
- 9.4.47
- 9.4.46
- 9.4.45
- 9.4.44
- 9.4.43
- 9.4.42
- 9.4.41
- 9.4.40
- 9.4.39
- 9.4.38
- 9.4.37
- 9.4.36
- 9.4.35
- 9.4.34
- 9.4.33
- 9.4.32
- 9.4.31
- 9.4.30
- 9.4.29
- 9.4.28
- 9.4.27
- 9.4.26
- 9.4.25
- 9.4.24
- 9.4.23
- 9.4.22
- 9.4.21
- 9.4.20
- 9.4.19
- 9.4.18
- 9.4.17
- 9.4.16
- 9.4.15
- 9.4.14
- 9.4.13
- 9.4.12
- 9.4.11
- 9.4.10
- 9.4.9
- 9.4.8
- 9.4.7
- 9.4.6
- 9.4.5
- 9.4.4
- 9.4.3
- 9.4.2
- v9.4.1
- v9.4.0
- v9.3.27
- v9.3.26
- v9.3.25
- v9.3.24
- v9.3.23
- v9.3.22
- v9.3.21
- v9.3.20
- v9.3.19
- v9.3.18
- v9.3.17
- v9.3.16
- v9.3.15
- v9.3.14
- v9.3.13
- v9.3.12
- v9.3.11
- v9.3.10
- v9.3.8
- v9.3.6
- v9.3.5
- v9.3.4
- v9.3.3
- v9.3.1
- v9.3.0
- v9.2.24
- v9.2.23
- v9.2.22
- v9.2.21
- v9.2.20
- v9.2.19
- v9.2.18
- v9.2.17
- v9.2.16
- v9.2.15
- v9.2.14
- v9.2.13
- v9.2.12
- v9.2.11
- v9.2.10
- v9.2.9
- v9.2.8
- v9.2.7
- v9.2.6
- v9.2.5
- v9.2.4
- v9.2.3
- v9.2.2
- 9.2.1
- 9.2.0
- 9.1.9
- 9.1.8
- 9.1.7
- 9.1.6
- 9.1.5
- 9.1.4
- 9.1.3
- 9.1.1
- 9.1.0
- 9.0.50
- 9.0.49
- 9.0.48
- 9.0.47
- 9.0.46
- 9.0.45
- 9.0.44
- 9.0.43
- 9.0.42
- 9.0.41
- 9.0.40
- 9.0.39
- 9.0.38
- 9.0.37
- 9.0.36
- 9.0.35
- 9.0.34
- 9.0.33
- 9.0.32
- 9.0.31
- 9.0.30
- 9.0.29
- 9.0.28
- 9.0.27
- 9.0.26
- 9.0.25
- 9.0.24
- 9.0.23
- 9.0.22
- 9.0.21
- 9.0.20
- 9.0.19
- 9.0.18
- 9.0.17
- 9.0.16
- 9.0.15
- 9.0.14
- 9.0.13
- 9.0.12
- 9.0.11
- 9.0.10
- 9.0.9
- 9.0.8
- 9.0.7
- 9.0.6
- 9.0.5
- 9.0.4
- 9.0.3
- 9.0.2
- 9.0.1
- 9.0.0
- 9.0.0-rc1
- 9.0.0-BETA9
- 9.0.0-BETA8
- 9.0.0-BETA7
- 9.0.0-BETA6
- 9.0.0-BETA5
- 9.0.0-BETA4
- 9.0.0-BETA3
- 9.0.0-BETA2
- 9.0.0-BETA1
- 8.3.48
- dev-remove-no-abstract-controller-constructor
- dev-naming-param
- dev-tv-exclude-brace-glob
- dev-no-get-repository-empty-args
- dev-remove-require-invokable-controller
- dev-worktree-form-type-suffix
- dev-worktree-remove-no-controller-method-injection
- dev-require-attribute-name-single-param
- dev-allow-fluent-setter
- dev-fix-parent-magic-method-reflection
- dev-tv/doctrine-and-config-rules
- dev-bump-deps-remove-downgrade
This package is auto-updated.
Last update: 2026-09-23 05:43:13 UTC
README
Set of 80+ PHPStan fun and practical rules that check:
- clean architecture, logical errors,
- naming, class namespace locations
- accidental visibility override,
- and Symfony, Doctrine or PHPUnit
bestproven practices.
Useful for any type of PHP project, from legacy to modern stack.
Install
composer require symplify/phpstan-rules --dev
Note: Make sure you use phpstan/extension-installer to load necessary service configs.
Usage
Configuration should be added to your phpstan.neon file.
Once you have most rules applied, it's best practice to include whole sets:
includes: - vendor/symplify/phpstan-rules/config/code-complexity-rules.neon - vendor/symplify/phpstan-rules/config/configurable-rules.neon - vendor/symplify/phpstan-rules/config/static-rules.neon # project specific - vendor/symplify/phpstan-rules/config/doctrine-rules.neon - vendor/symplify/phpstan-rules/config/symfony-rules.neon # special set for PHP configs - vendor/symplify/phpstan-rules/config/symfony-config-rules.neon
The naming rules (class/constant naming and namespace rules, e.g. interface must live in a "Contract" namespace) load automatically and are enabled by default. Turn them off with the naming parameter:
parameters: symplify: naming: false
Symfony/Laravel container ->get()/->make() return type extensions
Want sharper type inference? The return type extensions are disabled by default — enable the ones that fit your stack:
parameters: symplify: symfonyReturnType: true laravelReturnType: true pathStrings: true
symfonyReturnTyperesolves$container->get(SomeService::class)toSomeServiceand- Symfony Finder's
$splFileInfo->getRealPath()tostring
$service = $container->get(SomeService::class); // $service is now known as SomeService, instead of plain object
laravelReturnTypedoes the same for Laravel's$container->make(SomeService::class)pathStringsnarrowsgetcwd(),dirname()andrealpath()tostring:
But at start, make baby steps with one rule at a time:
Jump to: Symfony-specific rules, Doctrine-specific rules, PHPUnit-specific rules or PHPUnit mock rules.
Special rules
NewOverSettersRule
If a class is always created with the same set of setters, pass the values via constructor instead. It makes the object state explicit, safer and easier to test:
$human = new Human(); $human->setName('Tomas'); $human->setAge(35);
❌
$human = new Human(name: 'Tomas', age: 35);
👍
Both set* and add* method prefixes are treated as setters. The rule is intentionally conservative — it only reports a class instantiated at least twice with the same set of setters each time. It skips Doctrine entities, Symfony Kernel subclasses, vendor code and new + setters blocks interrupted by a return or throw.
This rule is disabled by default. Enable it with the ctor parameter:
parameters: symplify: ctor: true
ParamNameToTypeConventionRule
By convention, we can define parameter type by its name. If we know the "userId" is always an int, PHPStan can warn us about it and let us know to fill the type.
services: - class: Symplify\PHPStanRules\Rules\Convention\ParamNameToTypeConventionRule tags: [phpstan.rules.rule] arguments: paramNamesToTypes: userId: int
function run($userId) { }
❌
function run(int $userId) { }
👍
CheckRequiredInterfaceInContractNamespaceRule
Interface must be located in "Contract" or "Contracts" namespace
rules: - Symplify\PHPStanRules\Rules\CheckRequiredInterfaceInContractNamespaceRule
namespace App\Repository; interface ProductRepositoryInterface { }
❌
namespace App\Contract\Repository; interface ProductRepositoryInterface { }
👍
ClassNameRespectsParentSuffixRule
Class should have suffix "%s" to respect parent type
🔧 configure it!
services: - class: Symplify\PHPStanRules\Rules\ClassNameRespectsParentSuffixRule tags: [phpstan.rules.rule] arguments: parentClasses: - Symfony\Component\Console\Command\Command
↓
class Some extends Command { }
❌
class SomeCommand extends Command { }
👍
StringFileAbsolutePathExistsRule
Absolute file path must exist. Checked suffixes are "yaml", "yml", "sql", "php" and "json".
rules: - Symplify\PHPStanRules\Rules\StringFileAbsolutePathExistsRule
// missing file path return __DIR__ . '/some_file.yml';
❌
// correct file path return __DIR__ . '/../fixtures/some_file.yml';
👍
NoArrayMapWithArrayCallableRule
Array map with array callable is not allowed. Use anonymous/arrow function instead, to get better static analysis
rules: - Symplify\PHPStanRules\Rules\Complexity\NoArrayMapWithArrayCallableRule
$items = ['apple', 'banana', 'orange']; $items = array_map(['SomeClass', 'method'], $items);
❌
$items = ['apple', 'banana', 'orange']; $items = array_map(function ($item) { return $this->method($item); }, $items);
👍
NoConstructorOverrideRule
Possible __construct() override, this can cause missing dependencies or setup
rules: - Symplify\PHPStanRules\Rules\Complexity\NoConstructorOverrideRule
class ParentClass { public function __construct(private string $dependency) { } } class SomeClass extends ParentClass { public function __construct() { } }
❌
final class SomeClass extends ParentClass { public function __construct(private string $dependency) { } }
👍
ExplicitClassPrefixSuffixRule
Interface have suffix of "Interface", trait have "Trait" suffix exclusively
rules: - Symplify\PHPStanRules\Rules\Explicit\ExplicitClassPrefixSuffixRule
Reported under these identifiers:
- identifier: symplify.explicitAbstractPrefixName - identifier: symplify.explicitInterfaceSuffixName - identifier: symplify.explicitTraitSuffixName
<?php interface NotSuffixed { } trait NotSuffixed { } abstract class NotPrefixedClass { }
❌
<?php interface SuffixedInterface { } trait SuffixedTrait { } abstract class AbstractClass { }
👍
NoProtectedClassStmtRule
Avoid protected class stmts as they yield unexpected behavior. Use clear interface contract instead
rules: - Symplify\PHPStanRules\Rules\Explicit\NoProtectedClassStmtRule
ForbiddenArrayMethodCallRule
Array method calls [$this, "method"] are not allowed. Use explicit method instead to help PhpStorm, PHPStan and Rector understand your code
rules: - Symplify\PHPStanRules\Rules\Complexity\ForbiddenArrayMethodCallRule
usort($items, [$this, "method"]);
❌
usort($items, function (array $apples) { return $this->method($apples); };
👍
NoJustPropertyAssignRule
Instead of assigning service property to a variable, use the property directly
rules: - Symplify\PHPStanRules\Rules\Complexity\NoJustPropertyAssignRule
class SomeClass { // ... public function run() { $someService = $this->someService; $someService->run(); } }
❌
class SomeClass { // ... public function run() { $this->someService->run(); } }
👍
ForbiddenExtendOfNonAbstractClassRule
Only abstract classes can be extended
rules: - Symplify\PHPStanRules\Rules\ForbiddenExtendOfNonAbstractClassRule
final class SomeClass extends ParentClass { } class ParentClass { }
❌
abstract class ParentClass { }
👍
ForbiddenNewArgumentRule
Type "%s" is forbidden to be created manually with new X(). Use service and constructor injection instead
services: - class: Symplify\PHPStanRules\Rules\Complexity\ForbiddenNewArgumentRule tag: [phpstan.rules.rule] arguments: forbiddenTypes: - RepositoryService
↓
class SomeService { public function run() { $repositoryService = new RepositoryService(); $item = $repositoryService->get(1); } }
❌
class SomeService { public function __construct(private RepositoryService $repositoryService) { } public function run() { $item = $this->repositoryService->get(1); } }
👍
ForbiddenFuncCallRule
Function "%s()" cannot be used/left in the code
🔧 configure it!
services: - class: Symplify\PHPStanRules\Rules\ForbiddenFuncCallRule tags: [phpstan.rules.rule] arguments: forbiddenFunctions: - dump # or with custom error message dump: 'seems you missed some debugging function'
↓
dump('...');
❌
echo '...';
👍
ForbiddenMultipleClassLikeInOneFileRule
Multiple class/interface/trait is not allowed in single file
rules: - Symplify\PHPStanRules\Rules\ForbiddenMultipleClassLikeInOneFileRule
// src/SomeClass.php class SomeClass { } interface SomeInterface { }
❌
// src/SomeClass.php class SomeClass { } // src/SomeInterface.php interface SomeInterface { }
👍
ForbiddenNodeRule
"%s" is forbidden to use
🔧 configure it!
services: - class: Symplify\PHPStanRules\Rules\ForbiddenNodeRule tags: [phpstan.rules.rule] arguments: forbiddenNodes: - PhpParser\Node\Expr\ErrorSuppress
↓
return @strlen('...');
❌
return strlen('...');
👍
ForbiddenStaticClassConstFetchRule
Avoid static access of constants, as they can change value. Use interface and contract method instead
rules: - Symplify\PHPStanRules\Rules\ForbiddenStaticClassConstFetchRule
class SomeClass { public function run() { return static::SOME_CONST; } }
❌
class SomeClass { public function run() { return self::SOME_CONST; } }
👍
NoDynamicNameRule
Use explicit names over dynamic ones
rules: - Symplify\PHPStanRules\Rules\NoDynamicNameRule
class SomeClass { public function old(): bool { return $this->${variable}; } }
❌
class SomeClass { public function old(): bool { return $this->specificMethodName(); } }
👍
NoEntityOutsideEntityNamespaceRule
Class with #[Entity] attribute must be located in "Entity" namespace to be loaded by Doctrine
rules: - Symplify\PHPStanRules\Rules\NoEntityOutsideEntityNamespaceRule
namespace App\ValueObject; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class Product { }
❌
namespace App\Entity; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity] class Product { }
👍
NoGlobalConstRule
Global constants are forbidden. Use enum-like class list instead
rules: - Symplify\PHPStanRules\Rules\NoGlobalConstRule
const SOME_GLOBAL_CONST = 'value';
❌
class SomeClass { public function run() { return self::SOME_CONST; } }
👍
NoReferenceRule
Use explicit return value over magic &reference
rules: - Symplify\PHPStanRules\Rules\NoReferenceRule
class SomeClass { public function run(&$value) { } }
❌
class SomeClass { public function run($value) { return $value; } }
👍
NoReturnSetterMethodRule
Setter method cannot return anything, only set value
rules: - Symplify\PHPStanRules\Rules\NoReturnSetterMethodRule
final class SomeClass { private $name; public function setName(string $name): int { return 1000; } }
❌
final class SomeClass { private $name; public function setName(string $name): void { $this->name = $name; } }
👍
NoTestMocksRule
Mocking "%s" class is forbidden. Use direct/anonymous class instead for better static analysis
rules: - Symplify\PHPStanRules\Rules\PHPUnit\NoTestMocksRule
use PHPUnit\Framework\TestCase; final class SkipApiMock extends TestCase { public function test() { $someTypeMock = $this->createMock(SomeType::class); } }
❌
use PHPUnit\Framework\TestCase; final class SkipApiMock extends TestCase { public function test() { $someTypeMock = new class() implements SomeType {}; } }
👍
PreferredClassRule
Instead of "%s" class/interface use "%s"
🔧 configure it!
services: - class: Symplify\PHPStanRules\Rules\PreferredClassRule tags: [phpstan.rules.rule] arguments: oldToPreferredClasses: SplFileInfo: CustomFileInfo
↓
class SomeClass { public function run() { return new SplFileInfo('...'); } }
❌
class SomeClass { public function run() { return new CustomFileInfo('...'); } }
👍
PreventParentMethodVisibilityOverrideRule
Change "%s()" method visibility to "%s" to respect parent method visibility.
rules: - Symplify\PHPStanRules\Rules\PreventParentMethodVisibilityOverrideRule
class SomeParentClass { public function run() { } } class SomeClass extends SomeParentClass { protected function run() { } }
❌
class SomeParentClass { public function run() { } } class SomeClass extends SomeParentClass { public function run() { } }
👍
RequiredOnlyInAbstractRule
@required annotation should be used only in abstract classes, to child classes can use clean __construct() service injection.
rules: - Symplify\PHPStanRules\Rules\Symfony\RequiredOnlyInAbstractRule
RequireRouteNameToGenerateControllerRouteRule
To pass a controller class to generate() method, the controller must have "#[Route(name: self::class)]" above the __invoke() method
rules: - Symplify\PHPStanRules\Rules\Symfony\RequireRouteNameToGenerateControllerRouteRule
SingleRequiredMethodRule
There must be maximum 1 @required method in the class. Merge it to one to avoid possible injection collision or duplicated injects.
rules: - Symplify\PHPStanRules\Rules\Symfony\SingleRequiredMethodRule
RequireAttributeNameRule
Attribute must have all names explicitly defined
rules: - Symplify\PHPStanRules\Rules\RequireAttributeNameRule
use Symfony\Component\Routing\Annotation\Route; class SomeController { #[Route("/path")] public function someAction() { } }
❌
use Symfony\Component\Routing\Annotation\Route; class SomeController { #[Route(path: "/path")] public function someAction() { } }
👍
NoRouteTrailingSlashPathRule
Avoid trailing slash in route path, to prevent redirects and SEO issues
rules: - Symplify\PHPStanRules\Rules\Symfony\NoRouteTrailingSlashPathRule
RequireAttributeNamespaceRule
Attribute must be located in "Attribute" namespace
rules: - Symplify\PHPStanRules\Rules\Domain\RequireAttributeNamespaceRule
// app/Entity/SomeAttribute.php namespace App\Controller; #[\Attribute] final class SomeAttribute { }
❌
// app/Attribute/SomeAttribute.php namespace App\Attribute; #[\Attribute] final class SomeAttribute { }
👍
RequireExceptionNamespaceRule
Exception must be located in "Exception" namespace
rules: - Symplify\PHPStanRules\Rules\Domain\RequireExceptionNamespaceRule
// app/Controller/SomeException.php namespace App\Controller; final class SomeException extends Exception { }
❌
// app/Exception/SomeException.php namespace App\Exception; final class SomeException extends Exception { }
👍
RequireUniqueEnumConstantRule
Enum constants "%s" are duplicated. Make them unique instead
rules: - Symplify\PHPStanRules\Rules\Enum\RequireUniqueEnumConstantRule
use MyCLabs\Enum\Enum; class SomeClass extends Enum { private const YES = 'yes'; private const NO = 'yes'; }
❌
use MyCLabs\Enum\Enum; class SomeClass extends Enum { private const YES = 'yes'; private const NO = 'no'; }
👍
SeeAnnotationToTestRule
Class "%s" is missing @see annotation with test case class reference
🔧 configure it!
services: - class: Symplify\PHPStanRules\Rules\SeeAnnotationToTestRule tags: [phpstan.rules.rule] arguments: requiredSeeTypes: - Rule
↓
class SomeClass extends Rule { }
❌
/** * @see SomeClassTest */ class SomeClass extends Rule { }
👍
UppercaseConstantRule
Constant "%s" must be uppercase
rules: - Symplify\PHPStanRules\Rules\UppercaseConstantRule
final class SomeClass { public const some = 'value'; }
❌
final class SomeClass { public const SOME = 'value'; }
👍
ForeachCeptionRule
Avoid more than 3 nested foreach loops. Refactor to a flatter approach or to a collection to avoid high complexity
rules: - Symplify\PHPStanRules\Rules\Complexity\ForeachCeptionRule
foreach ($items as $item) { foreach ($item->getChildren() as $child) { foreach ($child->getGrandchildren() as $grandchild) { foreach ($grandchild->getData() as $data) { // ... } } } }
❌
foreach ($items as $item) { $this->processItem($item); }
👍
NoMissingVariableDimFetchRule
Dim fetch assign variable is missing, create it first
rules: - Symplify\PHPStanRules\Rules\Explicit\NoMissingVariableDimFetchRule
final class SomeClass { public function run() { $dim['key'] = 'value'; } }
❌
final class SomeClass { public function run() { $dim = []; $dim['key'] = 'value'; } }
👍
NoMissnamedDocTagRule
Constant doc comment tag must be @var, not @return or other tags
rules: - Symplify\PHPStanRules\Rules\NoMissnamedDocTagRule
class SomeConstant { /** * @return string */ private const NAME = 'value'; }
❌
class SomeConstant { /** * @var string */ private const NAME = 'value'; }
👍
NoValueObjectInServiceConstructorRule
Value object cannot be passed to constructor of a service. Pass it as a method argument instead
rules: - Symplify\PHPStanRules\Rules\NoValueObjectInServiceConstructorRule
final class SomeService { public function __construct(private SomeValueObject $someValueObject) { } }
❌
final class SomeService { public function run(SomeValueObject $someValueObject) { } }
👍
NoPropertyToPropertyAssignRule
An object property must not be assigned from another object property of the same object - it keeps the same service under 2 names. Use the original property directly instead.
rules: - Symplify\PHPStanRules\Rules\Complexity\NoPropertyToPropertyAssignRule
$this->repository = $this->someRepository;
❌
// use $this->someRepository directly
👍
NoDuplicateNonRepeatableAttributeRule
An attribute can only be repeated on the same class, method or property when it is declared with the \Attribute::IS_REPEATABLE flag.
rules: - Symplify\PHPStanRules\Rules\NoDuplicateNonRepeatableAttributeRule
#[SomeAttribute] #[SomeAttribute] private string $name;
❌
#[SomeAttribute] private string $name;
👍
RequireArrayShapeReturnRule
A method that returns a packed keyed array of 2-3 named values should declare that shape in its @return, so the caller knows each key and its type.
rules: - Symplify\PHPStanRules\Rules\RequireArrayShapeReturnRule
public function run(): array { return ['name' => $name, 'age' => $age]; }
❌
/** * @return array{name: string, age: int} */ public function run(): array { return ['name' => $name, 'age' => $age]; }
👍
2. Doctrine-specific Rules
NoStringTargetEntityRule
A Doctrine association attribute must reference its target entity as a class constant, not a string.
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoStringTargetEntityRule
#[ManyToOne(targetEntity: 'App\Entity\Category')]
❌
#[ManyToOne(targetEntity: Category::class)]
👍
NoReadonlyEntityClassRule
A Doctrine entity is hydrated via reflection without the constructor, so it must not be a readonly class. An entity is recognized by an #[ORM\Entity] attribute or a public static loadMetadata() method.
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoReadonlyEntityClassRule
#[ORM\Entity] final readonly class Product {}
❌
#[ORM\Entity] final class Product {}
👍
RequireQueryBuilderOnRepositoryRule
Prevents using $entityManager->createQueryBuilder('...'), use $repository->createQueryBuilder() as safer.
rules: - Symplify\PHPStanRules\Rules\Doctrine\RequireQueryBuilderOnRepositoryRule
NoGetRepositoryOutsideServiceRule
Instead of getting repository from EntityManager, use constructor injection and service pattern to keep code clean
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOutsideServiceRule
class SomeClass { public function run(EntityManagerInterface $entityManager) { return $entityManager->getRepository(SomeEntity::class); } }
❌
class SomeClass { public function __construct(SomeEntityRepository $someEntityRepository) { } }
👍
NoParentRepositoryRule
Repository should not extend parent repository, as it can lead to tight coupling
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoParentRepositoryRule
use Doctrine\ORM\EntityRepository; final class SomeRepository extends EntityRepository { }
❌
final class SomeRepository { public function __construct(EntityManagerInterface $entityManager) { $this->repository = $entityManager->getRepository(SomeEntity::class); } }
👍
NoGetRepositoryOnServiceRepositoryEntityRule
Instead of calling "->getRepository(...::class)" service locator, inject service repository via constructor and use it directly
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOnServiceRepositoryEntityRule
use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass=SomeRepository::class) */ class SomeEntity { }
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; final class SomeEntityRepository extends ServiceEntityRepository { }
use Doctrine\ORM\EntityManagerInterface; final class SomeService { public function run(EntityManagerInterface $entityManager) { return $this->entityManager->getRepository(SomeEntity::class); } }
❌
use Doctrine\ORM\EntityManagerInterface; final class SomeService { public function __construct(private SomeEntityRepository $someEntityRepository) { } }
👍
NoRepositoryCallInDataFixtureRule
Repository should not be called in data fixtures, as it can lead to tight coupling
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoRepositoryCallInDataFixtureRule
use Doctrine\Common\DataFixtures\AbstractFixture; final class SomeFixture extends AbstractFixture { public function load(ObjectManager $objectManager) { $someRepository = $objectManager->getRepository(SomeEntity::class); $someEntity = $someRepository->get(1); } }
❌
use Doctrine\Common\DataFixtures\AbstractFixture; final class SomeFixture extends AbstractFixture { public function load(ObjectManager $objectManager) { $someEntity = $this->getReference('some-entity-1'); } }
👍
3. Symfony-specific Rules
FormTypeClassNameRule
rules: - Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule
Classes that extend AbstractType should have *Type suffix, to make it clear it's a form class.
NoConstructorAndRequiredTogetherRule
Constructor injection and #[Required] method should not be used together in single class. Pick one, to keep architecture clean.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoConstructorAndRequiredTogetherRule
NoGetDoctrineInControllerRule
Prevents using $this->getDoctrine() in controllers, to promote dependency injection.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoGetDoctrineInControllerRule
NoGetInControllerRule
Prevents using $this->get(...) in controllers, to promote dependency injection.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoGetInControllerRule
TaggedIteratorOverRepeatedServiceCallRuleTest
Instead of repeated "->call(%s, ...)" calls, pass services as tagged iterator argument to the constructor
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\TaggedIteratorOverRepeatedServiceCallRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\ref; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class) ->call('setService', [ref('service1')]) ->call('setService', [ref('service2')]) ->call('setService', [ref('service3')]); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; use Symfony\Component\DependencyInjection\Loader\Configurator\tagged_iterator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class) ->arg('$services', tagged_iterator('SomeServiceTag')); };
👍
NoGetInCommandRule
Prevents using $this->get(...) in commands, to promote dependency injection.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoGetInCommandRule
NoServiceSameNameSetClassRule
No need to duplicate service class and name. Use only "$services->set(%s::class)" instead
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoServiceSameNameSetClassRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class, SomeService::class); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class); };
PreferAutowireAttributeOverConfigParamRule
Instead of parameter reference in config, add #[Autowire(param: ...)] in the "%s" class constructor
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\PreferAutowireAttributeOverConfigParamRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class)->args(['%some_param%']); };
❌
use Symfony\Component\DependencyInjection\Attribute\Autowire; final class SomeService { public function __construct( #[Autowire(param: 'some_param')] private string $someParam ) { } }
👍
NoDuplicateArgsAutowireByTypeRule
Instead of passing "%s" to args(), remove the line and let autowiring handle it
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoDuplicateArgAutowireByTypeRule - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoDuplicateArgsAutowireByTypeRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class) ->args([ ref(SomeService::class), ]); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class); };
👍
AlreadyRegisteredAutodiscoveryServiceRule
Remove service, as already registered via autodiscovery ->load(), no need to set it twice.
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\AlreadyRegisteredAutodiscoveryServiceRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->load('App\\', __DIR__ . '/../src') ->exclude([__DIR__ . '/src/Services']); $services->set(SomeService::class); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->load('App\\', __DIR__ . '/../src') ->exclude([__DIR__ . '/src/Services']); };
👍
ServicesExcludedDirectoryMustExistRule
Services excluded path must exist. If not, remove it
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\ServicesExcludedDirectoryMustExistRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $configurator): void { $services = $configurator->serivces(); $services->load('App\\', __DIR__ . '/../src') ->exclude([__DIR__ . '/this-path-does-not-exist']); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $configurator): void { $services = $configurator->services(); $services->load('App\\', __DIR__ . '/../src') ->exclude([__DIR__ . '/../src/ValueObject']); };
👍
NoBundleResourceConfigRule
Avoid using configs in *Bundle/Resources directory. Move them to /config directory instead
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoBundleResourceConfigRule
NoBareAndSecurityIsGrantedContentsRule
Instead of using one long "and" condition join, split into multiple standalone #[IsGranted] attributes
rules: - Symplify\PHPStanRules\Rules\Symfony\NoBareAndSecurityIsGrantedContentsRule
use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('has_role(ROLE_USER) and has_role(ROLE_ADMIN)')] class SomeController { }
❌
use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('ROLE_USER')] #[IsGranted('ROLE_ADMIN')] class SomeController { }
👍
RequireIsGrantedEnumRule
Instead of string, use enum constant for #[IsGranted]
rules: - Symplify\PHPStanRules\Rules\Symfony\RequireIsGrantedEnumRule
use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted('ROLE_USER')] class SomeController { }
❌
use Symfony\Component\Security\Http\Attribute\IsGranted; #[IsGranted(SomeEnum::ROLE_USER)] class SomeController { }
👍
NoRoutingPrefixRule
Avoid global route prefixing. Use single place for paths in @Route/#[Route] and improve static analysis instead.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoRoutingPrefixRule
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return static function (RoutingConfigurator $routingConfigurator): void { $routingConfigurator->import(__DIR__ . '/some-path') ->prefix('/some-prefix'); };
❌
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; return static function (RoutingConfigurator $routingConfigurator): void { $routingConfigurator->import(__DIR__ . '/some-path'); };
👍
NoClassLevelRouteRule
Avoid class-level route prefixing. Use method route to keep single source of truth and focus
rules: - Symplify\PHPStanRules\Rules\Symfony\NoClassLevelRouteRule
use Symfony\Component\Routing\Attribute\Route; #[Route('/some-prefix')] class SomeController { #[Route('/some-action')] public function someAction() { } }
❌
use Symfony\Component\Routing\Attribute\Route; class SomeController { #[Route('/some-prefix/some-action')] public function someAction() { } }
👍
NoFindTaggedServiceIdsCallRule
Instead of "$this->findTaggedServiceIds()" use more reliable registerForAutoconfiguration() and tagged iterator attribute. Those work outside any configuration and avoid missed tag errors
rules: - Symplify\PHPStanRules\Rules\Symfony\NoFindTaggedServiceIdsCallRule
NoRequiredOutsideClassRule
Symfony #[Require]/@required should be used only in classes to avoid misuse
rules: - Symplify\PHPStanRules\Rules\Symfony\NoRequiredOutsideClassRule
use Symfony\Component\DependencyInjection\Attribute\Required; trait SomeTrait { #[Required] public function autowireSomeTrait(SomeService $someService) { // ... } }
❌
abstract class SomeClass { #[Required] public function autowireSomeClass(SomeService $someService) { // ... } }
👍
SingleArgEventDispatchRule
The event dispatch() method can have only 1 arg - the event object
rules: - Symplify\PHPStanRules\Rules\Symfony\SingleArgEventDispatchRule
use Symfony\Component\EventDispatcher\EventDispatcherInterface; final class SomeClass { public function __construct( private EventDispatcherInterface $eventDispatcher ) { } public function run() { $this->eventDispatcher->dispatch('event', 'another-arg'); } }
❌
use Symfony\Component\EventDispatcher\EventDispatcherInterface; final class SomeClass { public function __construct( private EventDispatcherInterface $eventDispatcher ) { } public function run() { $this->eventDispatcher->dispatch(new EventObject()); } }
👍
NoListenerWithoutContractRule
There should be no listeners modified in config. Use EventSubscriberInterface contract or #[AsEventListener] attribute and PHP instead
rules: - Symplify\PHPStanRules\Rules\Symfony\NoListenerWithoutContractRule
class SomeListener { public function onEvent() { } }
❌
use Symfony\Component\EventDispatcher\EventSubscriberInterface; class SomeListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ 'event' => 'onEvent', ]; } public function onEvent() { } }
👍
use Symfony\Component\EventDispatcher\Attribute\AsEventListener; #[AsEventListener] class SomeListener { public function __invoke() { } }
👍
RequireServiceRepositoryParentRule
Repository must extend *, so it can be injected as a service
rules: - Symplify\PHPStanRules\Rules\Doctrine\RequireServiceRepositoryParentRule
final class SomeRepository { public function __construct(EntityManagerInterface $entityManager) { // ... } }
❌
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; final class SomeRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { parent::__construct($registry, SomeEntity::class); } }
👍
NoDoctrineListenerWithoutContractRule
There should be no Doctrine listeners modified in config. Implement "Document\Event\EventSubscriber" to provide events in the class itself
rules: - Symplify\PHPStanRules\Rules\Doctrine\NoDoctrineListenerWithoutContractRule
class SomeListener { public function onFlush() { } }
❌
use Doctrine\Common\EventSubscriber; use Doctrine\ODM\MongoDB\Events; class SomeListener implements EventSubscriber { public function onFlush() { } public static function getSubscribedEvents(): array { return [ Events::onFlush ]; } }
👍
NoStringInGetSubscribedEventsRule
Symfony getSubscribedEvents() method must contain only event class references, no strings
rules: - Symplify\PHPStanRules\Rules\Symfony\NoStringInGetSubscribedEventsRule
use Symfony\Component\EventDispatcher\EventSubscriberInterface; class SomeListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ 'event' => 'onEvent', ]; } public function onEvent() { } }
❌
use Symfony\Component\EventDispatcher\EventSubscriberInterface; class SomeListener implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ Event::class => 'onEvent', ]; } public function onEvent() { } }
👍
NoServiceAutowireDuplicateRule
Service autowire() is called as a duplicate of $services->defaults()->autowire(). Remove it on the service.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoServiceAutowireDuplicateRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->defaults()->autowire(); $services->set('some_service') ->autowire(); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->defaults()->autowire(); $services->set('some_service'); };
👍
NoSetClassServiceDuplicationRule
Instead of $services->set(X)->class(X) that brings no value, use simple $services->set(X)
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoSetClassServiceDuplicationRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class) ->class(SomeService::class); };
❌
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(SomeService::class); };
👍
FileNameMatchesExtensionRule
The config uses a specific extension, but the file name does not match. Sync them to ease discovery.
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\FileNameMatchesExtensionRule
// wrong_name.php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $container) { $framework = $container->extension('framework'); };
❌
// framework.php use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return function (ContainerConfigurator $container) { $framework = $container->extension('framework'); };
👍
CommandMustHaveAsCommandAttributeRule
Every class that extends Symfony Command must declare the #[AsCommand] attribute.
rules: - Symplify\PHPStanRules\Rules\Symfony\CommandMustHaveAsCommandAttributeRule
final class ReportCommand extends Command { }
❌
#[AsCommand('app:report')] final class ReportCommand extends Command { }
👍
ConstraintMustHaveAttributeRule
Every class that extends Symfony Constraint must declare the #[\Attribute] attribute, so it can be used as an attribute on properties.
rules: - Symplify\PHPStanRules\Rules\Symfony\ConstraintMustHaveAttributeRule
final class UniqueEmail extends Constraint { }
❌
#[\Attribute] final class UniqueEmail extends Constraint { }
👍
PreferInterfaceInConstructorRule
A constructor dependency typed as a concrete Symfony/Doctrine class that has a same-named *Interface should use that interface instead - it blocks decoration otherwise.
rules: - Symplify\PHPStanRules\Rules\Symfony\PreferInterfaceInConstructorRule
public function __construct( private Router $router, ) { }
❌
public function __construct( private RouterInterface $router, ) { }
👍
NoServiceJugglingRule
A service injected in __construct() or an autowire*() method must not be passed to another method of the same class. The called method has its own constructor, so it can take the service directly instead of receiving it through a parameter list.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoServiceJugglingRule
public function run(): void { $this->handle($this->userHelper); } public function handle(UserHelper $userHelper): void { }
❌
public function __construct( private readonly UserHelper $userHelper, ) { } public function run(): void { $this->handle(); } public function handle(): void { // use $this->userHelper directly }
👍
NoNullableServiceInConstructorRule
A constructor service dependency must not be nullable - a service is always provided by the container, so ?SomeService only hides that it is really required. Nullable is allowed on an abstract class (a child fills the dependency), on values that are not services (scalars, arrays, exceptions like ?Throwable $previous, date value objects), and on data-holder classes in an Entity, Event, DTO, Message, DAO, Token, Exception, Helper, ValueObject, Form\Type or Badge namespace.
rules: - Symplify\PHPStanRules\Rules\Symfony\NoNullableServiceInConstructorRule
public function __construct( private readonly ?SomeService $someService, ) { }
❌
public function __construct( private readonly SomeService $someService, ) { }
👍
PreferClassServiceReferenceRule
In a Symfony PHP config closure, when a $services->alias('some.helper', SomeHelper::class) points a string id at a class, a service('some.helper') reference should name the service by its class instead - service(SomeHelper::class). Then the string alias nothing else asks for can be dropped.
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\PreferClassServiceReferenceRule
$services->alias('some.helper', SomeHelper::class); $services->set(SomeConsumer::class) ->args([service('some.helper')]);
❌
$services->alias('some.helper', SomeHelper::class); $services->set(SomeConsumer::class) ->args([service(SomeHelper::class)]);
👍
PreferClassInDefinitionFetchRule
A container definition fetch that names a class by a plain string should use the class constant instead. The string is only flagged when it is a real class name; a service id string is left alone.
rules: - Symplify\PHPStanRules\Rules\Symfony\PreferClassInDefinitionFetchRule
$container->getDefinition('App\Helper\ColumnSchemaHelper');
❌
$container->getDefinition(ColumnSchemaHelper::class);
👍
NoServiceSetterCallRule
In a PHP config closure, a setter injection wired by hand with ->call('setX', [service(...)]) should be a #[Required] attribute on the setter, so autowiring calls it. Only a setXxx() method fed a service() is reported; a container parameter or a non-setter call stays.
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoServiceSetterCallRule
$services->set(SomeService::class) ->call('setRepository', [service(SomeRepository::class)]);
❌
#[Required] public function setRepository(SomeRepository $someRepository): void { $this->someRepository = $someRepository; }
👍
NoAutoconfiguredServiceTagRule
When a PHP config closure's defaults() uses autoconfigure(), a ->tag() that autoconfiguration already adds by interface (console.command, form.type, kernel.event_subscriber, security.voter, twig.extension, validator.constraint_validator) is redundant. Only a tag with no attributes of its own is reported.
rules: - Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoAutoconfiguredServiceTagRule
$services->defaults()->autoconfigure(); $services->set(SomeSubscriber::class) ->tag('kernel.event_subscriber');
❌
$services->defaults()->autoconfigure(); $services->set(SomeSubscriber::class);
👍
4. PHPUnit-specific Rules
NoAssertFuncCallInTestsRule
Avoid using assert*() functions in tests, as they can lead to false positives
rules: - Symplify\PHPStanRules\Rules\PHPUnit\NoAssertFuncCallInTestsRule
PublicStaticDataProviderRule
PHPUnit data provider method "%s" must be public
rules: - Symplify\PHPStanRules\Rules\PHPUnit\PublicStaticDataProviderRule
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { /** * @dataProvider dataProvider */ public function test(): array { return []; } protected function dataProvider(): array { return []; } }
❌
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { /** * @dataProvider dataProvider */ public function test(): array { return []; } public static function dataProvider(): array { return []; } }
👍
5. PHPUnit Mock Rules
- Do you use extensive mocking in your PHPUnit tests?
- Do you want to keep your tests clean, maintainable and avoid upgrade hell in the future?
- Do you want to have tests that actually test something?
This set is for you! Enable all mocking rules with single parameter in your phpstan.neon:
parameters: symplify: mocks: true
NoMockOnlyTestRule
Test should have at least one non-mocked property, to test something
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SomeTest extends TestCase { private MockObject $firstMock; private MockObject $secondMock; public function setUp() { $this->firstMock = $this->createMock(SomeService::class); $this->secondMock = $this->createMock(AnotherService::class); } }
❌
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; class SomeTest extends TestCase { private SomeService $someService; private MockObject $firstMock; public function setUp() { $this->someService = new SomeService(); $this->firstMock = $this->createMock(AnotherService::class); } }
👍
NoMockObjectAndRealObjectPropertyRule
Avoid using one property for both real object and mock object. Use separate properties or single type instead
$this->service = $this->createMock(Service::class); $this->service = new Service();
❌
$this->someMock = $this->createMock(AnotherService::class); $this->realService = new Service();
👍
NoDoubleConsecutiveTestMockRule
Do not use willReturnOnConsecutiveCalls() and willReturnCallback() on the same mock. Use willReturnCallback() only instead to make the test more clear.
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { public function test() { $this->createMock('SomeClass') ->expects($this->exactly(2)) ->method('someMethod') ->willReturnCallback(function () { return 'first'; }) ->willReturnOnConsecutiveCalls('first'); } }
❌
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { public function test() { $this->createMock('SomeClass') ->expects($this->exactly(2)) ->method('someMethod') ->willReturnCallback(function () { return 'first'; }); } }
👍
ExplicitExpectsMockMethodRule
PHPUnit mock method is missing explicit expects(), e.g. $this->mock->expects($this->once())->.... This is required since PHPUnit 12 to avoid silent stubs.
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { public function test(): void { $mock = $this->createMock(\stdClass::class); $mock->method('someMethod')->willReturn('value'); } }
❌
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { public function test(): void { $mock = $this->createMock(\stdClass::class); $mock->expects($this->atLeastOnce()) ->method('someMethod') ->willReturn('value'); } }
👍
AvoidAnyExpectsRule
Disallow usage of any() expectation in mocks to ensure that all mock interactions are explicitly defined and verified.
$someMock = $this->createMock(Service::class); $someMock->expects($this->any()) ->method('calculate') ->willReturn(10);
❌
$someMock = $this->createMock(Service::class); $someMock->expects($this->once()) ->method('calculate') ->willReturn(10);
👍
NoWithOnStubRule
Disallow with() on stubs (mocks without expects()). PHPUnit deprecates with() on test stubs because they silently swallow argument mismatches.
$someMock = $this->createMock(Service::class); $someMock->method('calculate') ->with(10) ->willReturn(20);
❌
$someMock = $this->createMock(Service::class); $someMock->expects($this->once()) ->method('calculate') ->with(10) ->willReturn(20);
👍
RequireAtLeastOneRule
Disallow atLeast(0) on mock expectations, as it matches any number of calls (including zero) and provides no real verification. Require a value of 1 or higher.
$someMock = $this->createMock(Service::class); $someMock->expects($this->atLeast(0)) ->method('calculate') ->willReturn(10);
❌
$someMock = $this->createMock(Service::class); $someMock->expects($this->atLeast(1)) ->method('calculate') ->willReturn(10);
👍
NoEntityMockingRule, NoDocumentMockingRule
Instead of entity or document mocking, create object directly to get better type support
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { public function test() { $someEntityMock = $this->createMock(SomeEntity::class); } }
❌
use PHPUnit\Framework\TestCase; final class SomeTest extends TestCase { public function test() { $someEntityMock = new SomeEntity(); } }
👍
6. Type Extensions and Error Formatter
These extensions were merged from the now-deprecated symplify/phpstan-extensions
package. They load automatically once phpstan/extension-installer is set up - no extra
configuration is needed.
symplify Error Formatter
A compact error format with pre-escaped, regex-ready messages that are easy to copy into
your ignoreErrors list. File paths are printed with line numbers and stay clickable in
the terminal (works best with anthraxx/intellij-awesome-console).
Enable it in your phpstan.neon:
parameters: errorFormat: symplify
or on the command line:
vendor/bin/phpstan analyse --error-format symplify
Type Extensions
Always-on return type extensions that sharpen PHPStan inference for common framework calls:
-
ContainerGetReturnTypeExtension-$container->get(SomeService::class)returnsSomeServiceinstead of plainobject(SymfonyContainerInterface). -
LaravelContainerMakeTypeExtension-$container->make(SomeService::class)and->get(SomeService::class)returnSomeService(LaravelIlluminate\Container\Container). -
SplFileInfoTolerantReturnTypeExtension-$splFileInfo->getRealPath()returnsstringinstead ofstring|false, as Symfony Finder only yields existing files. -
NativeFunctionReturnTypeExtension-getcwd(),dirname()andrealpath()returnstringinstead ofstring|false.
Happy coding!