Skip to content

symplify/phpstan-rules

Repository files navigation

PHPStan Rules

Downloads

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.


Install

composer require symplify/phpstan-rules --dev

Note: Make sure you use phpstan/extension-installer to load necessary service configs.


Usage

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:


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
{
}

πŸ‘


NoConstructorOverrideRule

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)
    {
    }
}

πŸ‘


ExplicitClassPrefixSuffixRule

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
{
}

πŸ‘


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);
};

πŸ‘


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\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()
    {
    }
}

πŸ‘


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()
    {
    }
}

πŸ‘


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';
}

πŸ‘




2. Doctrine-specific Rules

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);
    }
}

πŸ‘


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

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

NoAbstractControllerConstructorRule

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;
    }
}

πŸ‘


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 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()
    {
    }
}

πŸ‘


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()
    {
    }
}

πŸ‘


RequireInvokableControllerRule

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()
    {
    }
}

πŸ‘




4. PHPUnit-specific Rules

NoEntityMockingRule, NoDocumentMockingRule

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();
    }
}

πŸ‘


NoMockOnlyTestRule

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);
    }
}

πŸ‘


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 [];
    }
}

πŸ‘


Happy coding!