Set of 65+ 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.
composer require symplify/phpstan-rules --dev
Note: Make sure you use phpstan/extension-installer
to load necessary service configs.
Later, 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/naming-rules.neon
- vendor/symplify/phpstan-rules/config/static-rules.neon
# project specific
- vendor/symplify/phpstan-rules/config/rector-rules.neon
- vendor/symplify/phpstan-rules/config/doctrine-rules.neon
- vendor/symplify/phpstan-rules/config/symfony-rules.neon
But at start, make baby steps with one rule at a time:
Jump to: Symfony-specific rules, Doctrine-specific rules or PHPUnit-specific rules.
Tired of ever growing ignored error count in your phpstan.neon
? Set hard limit to keep them low:
parameters:
maximumIgnoredErrorCount: 50
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)
{
}
π
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
{
}
π
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
{
}
π
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';
π
Possible __construct() override, this can cause missing dependencies or setup
rules:
- Symplify\PHPStanRules\Rules\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)
{
}
}
π
Interface have suffix of "Interface", trait have "Trait" suffix exclusively
rules:
- Symplify\PHPStanRules\Rules\Explicit\ExplicitClassPrefixSuffixRule
<?php
interface NotSuffixed
{
}
trait NotSuffixed
{
}
abstract class NotPrefixedClass
{
}
β
<?php
interface SuffixedInterface
{
}
trait SuffixedTrait
{
}
abstract class AbstractClass
{
}
π
Avoid protected class stmts as they yield unexpected behavior. Use clear interface contract instead
rules:
- Symplify\PHPStanRules\Rules\Explicit\NoProtectedClassStmtRule
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);
};
π
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();
}
}
π
Only abstract classes can be extended
rules:
- Symplify\PHPStanRules\Rules\ForbiddenExtendOfNonAbstractClassRule
final class SomeClass extends ParentClass
{
}
class ParentClass
{
}
β
abstract class ParentClass
{
}
π
Type "%s" is forbidden to be created manually with new X()
. Use service and constructor injection instead
services:
-
class: Symplify\PHPStanRules\Rules\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);
}
}
π
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 '...';
π
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
{
}
π
"%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('...');
π
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;
}
}
π
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();
}
}
π
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
{
}
π
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;
}
}
π
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;
}
}
π
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;
}
}
π
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 {};
}
}
π
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('...');
}
}
π
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()
{
}
}
π
@required
annotation should be used only in abstract classes, to child classes can use clean __construct()
service injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\RequiredOnlyInAbstractRule
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
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
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()
{
}
}
π
Avoid trailing slash in route path, to prevent redirects and SEO issues
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoRouteTrailingSlashPathRule
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
{
}
π
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
{
}
π
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';
}
π
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
{
}
π
Constant "%s" must be uppercase
rules:
- Symplify\PHPStanRules\Rules\UppercaseConstantRule
final class SomeClass
{
public const some = 'value';
}
β
final class SomeClass
{
public const SOME = 'value';
}
π
Prevents using $entityManager->createQueryBuilder('...')
, use $repository->createQueryBuilder()
as safer.
rules:
- Symplify\PHPStanRules\Rules\Doctrine\RequireQueryBuilderOnRepositoryRule
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)
{
}
}
π
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);
}
}
π
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)
{
}
}
π
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');
}
}
π
rules:
- Symplify\PHPStanRules\Rules\Symfony\FormTypeClassNameRule
Classes that extend AbstractType
should have *FormType
suffix, to make it clear it's a form class.
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
Prevents using $this->getDoctrine()
in controllers, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetDoctrineInControllerRule
Prevents using $this->get(...)
in controllers, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetInControllerRule
Prevents using $this->get(...)
in commands, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetInCommandRule
Abstract controller should not have constructor, as it can lead to tight coupling. Use @required annotation instead
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoAbstractControllerConstructorRule
abstract class AbstractController extends Controller
{
public function __construct(
private SomeService $someService
) {
}
}
β
abstract class AbstractController extends Controller
{
private $someService;
#[Required]
public function autowireAbstractController(SomeService $someService)
{
$this->someService = $someService;
}
}
π
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');
};
π
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()
{
}
}
π
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
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)
{
// ...
}
}
π
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());
}
}
π
There should be no listeners modified in config. Use EventSubscriberInterface contract 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()
{
}
}
π
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()
{
}
}
π
Use invokable controller with __invoke() method instead of named action method
rules:
- Symplify\PHPStanRules\Rules\Symfony\RequireInvokableControllerRule
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;
final class SomeController extends AbstractController
{
#[Route()]
public function someMethod()
{
}
}
β
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
final class SomeController extends AbstractController
{
#[Route()]
public function __invoke()
{
}
}
π
Avoid using one property for both real object and mock object. Use separate properties or single type instead
rules:
- Symplify\PHPStanRules\Rules\PHPUnit\NoMockObjectAndRealObjectPropertyRule
Instead of entity or document mocking, create object directly to get better type support
rules:
- Symplify\PHPStanRules\Rules\PHPUnit\NoEntityMockingRule
- Symplify\PHPStanRules\Rules\PHPUnit\NoDocumentMockingRule
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();
}
}
π
Avoid using assert*() functions in tests, as they can lead to false positives
rules:
- Symplify\PHPStanRules\Rules\PHPUnit\NoAssertFuncCallInTestsRule
Test should have at least one non-mocked property, to test something
rules:
- Symplify\PHPStanRules\Rules\PHPUnit\NoMockOnlyTestRule
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 FirstMock $firstMock;
public function setUp()
{
$this->someService = new SomeService();
$this->firstMock = $this->createMock(AnotherService::class);
}
}
π
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 [];
}
}
π
Happy coding!