Set of 70+ 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 --devNote: Make sure you use phpstan/extension-installer to load necessary service configs.
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/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
# special set for PHP configs
- vendor/symplify/phpstan-rules/config/symfony-config-rules.neonBut 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: 50By 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: intfunction run($userId)
{
}β
function run(int $userId)
{
}π
Interface must be located in "Contract" or "Contracts" namespace
rules:
- Symplify\PHPStanRules\Rules\CheckRequiredInterfaceInContractNamespaceRulenamespace 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';π
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);π
Possible __construct() override, this can cause missing dependencies or setup
rules:
- Symplify\PHPStanRules\Rules\Complexity\NoConstructorOverrideRuleclass 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\NoProtectedClassStmtRuleArray 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\ForbiddenArrayMethodCallRuleusort($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\NoJustPropertyAssignRuleclass 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\ForbiddenExtendOfNonAbstractClassRulefinal 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\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);
}
}π
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\ForbiddenStaticClassConstFetchRuleclass 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\NoDynamicNameRuleclass 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\NoEntityOutsideEntityNamespaceRulenamespace 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\NoGlobalConstRuleconst SOME_GLOBAL_CONST = 'value';β
class SomeClass
{
public function run()
{
return self::SOME_CONST;
}
}π
Use explicit return value over magic &reference
rules:
- Symplify\PHPStanRules\Rules\NoReferenceRuleclass 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\NoReturnSetterMethodRulefinal 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\NoTestMocksRuleuse 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\PreventParentMethodVisibilityOverrideRuleclass 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\RequiredOnlyInAbstractRuleTo pass a controller class to generate() method, the controller must have "#[Route(name: self::class)]" above the __invoke() method
rules:
- Symplify\PHPStanRules\Rules\Symfony\RequireRouteNameToGenerateControllerRouteRuleThere 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\SingleRequiredMethodRuleAttribute must have all names explicitly defined
rules:
- Symplify\PHPStanRules\Rules\RequireAttributeNameRuleuse 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\NoRouteTrailingSlashPathRuleAttribute 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\RequireUniqueEnumConstantRuleuse 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\UppercaseConstantRulefinal 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\RequireQueryBuilderOnRepositoryRuleInstead of getting repository from EntityManager, use constructor injection and service pattern to keep code clean
rules:
- Symplify\PHPStanRules\Rules\Doctrine\NoGetRepositoryOutsideServiceRuleclass 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\NoParentRepositoryRuleuse 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\NoGetRepositoryOnServiceRepositoryEntityRuleuse 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\NoRepositoryCallInDataFixtureRuleuse 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\FormTypeClassNameRuleClasses 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\NoConstructorAndRequiredTogetherRulePrevents using $this->getDoctrine() in controllers, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetDoctrineInControllerRulePrevents using $this->get(...) in controllers, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetInControllerRuleInstead of repeated "->call(%s, ...)" calls, pass services as tagged iterator argument to the constructor
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\TaggedIteratorOverRepeatedServiceCallRuleuse 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'));
};π
Prevents using $this->get(...) in commands, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetInCommandRuleNo need to duplicate service class and name. Use only "$services->set(%s::class)" instead
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoServiceSameNameSetClassRuleuse 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);
};Instead of parameter reference in config, add #[Autowire(param: ...)] in the "%s" class constructor
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\PreferAutowireAttributeOverConfigParamRuleuse 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
) {
}
}π
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\NoDuplicateArgsAutowireByTypeRuleuse 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);
};π
Abstract controller should not have constructor, as it can lead to tight coupling. Use @required annotation instead
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoAbstractControllerConstructorRuleabstract 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;
}
}π
Remove service, as already registered via autodiscovery ->load(), no need to set it twice.
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\AlreadyRegisteredAutodiscoveryServiceRuleuse 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']);
};π
Services excluded path must exist. If not, remove it
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\ServicesExcludedDirectoryMustExistRuleuse 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']);
};π
Avoid using configs in *Bundle/Resources directory. Move them to /config directory instead
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoBundleResourceConfigRuleInstead of using one long "and" condition join, split into multiple standalone #[IsGranted] attributes
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoBareAndSecurityIsGrantedContentsRuleuse 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
{
}π
Instead of string, use enum constant for #[IsGranted]
rules:
- Symplify\PHPStanRules\Rules\Symfony\RequireIsGrantedEnumRuleuse Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_USER')]
class SomeController
{
}β
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted(SomeEnum::ROLE_USER)]
class SomeController
{
}π
Avoid global route prefixing. Use single place for paths in @Route/#[Route] and improve static analysis instead.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoRoutingPrefixRuleuse 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\NoClassLevelRouteRuleuse 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\NoFindTaggedServiceIdsCallRuleSymfony #[Require]/@required should be used only in classes to avoid misuse
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoRequiredOutsideClassRuleuse 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\SingleArgEventDispatchRuleuse 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 or #[AsEventListener] attribute and PHP instead
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoListenerWithoutContractRuleclass 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()
{
}
}π
Repository must extend *, so it can be injected as a service
rules:
- Symplify\PHPStanRules\Rules\Doctrine\RequireServiceRepositoryParentRulefinal 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);
}
}π
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\NoDoctrineListenerWithoutContractRuleclass 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
];
}
}π
Symfony getSubscribedEvents() method must contain only event class references, no strings
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoStringInGetSubscribedEventsRuleuse 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\RequireInvokableControllerRuleuse 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\NoMockObjectAndRealObjectPropertyRuleInstead of entity or document mocking, create object directly to get better type support
rules:
- Symplify\PHPStanRules\Rules\Doctrine\NoEntityMockingRule
- Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRuleuse 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\NoAssertFuncCallInTestsRuleTest should have at least one non-mocked property, to test something
rules:
- Symplify\PHPStanRules\Rules\PHPUnit\NoMockOnlyTestRuleuse 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\PublicStaticDataProviderRuleuse 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!