Set of 35 custom PHPStan rules that check architecture, typos, class namespace locations, accidental visibility override and more. 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:
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
{
}
π
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
{
}
π
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);
};
π
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()
{
}
}
π
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()
{
}
}
π
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);
}
}
π
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');
}
}
π
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
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;
}
}
π
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()
{
}
}
π
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();
}
}
π
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!