symplify/phpstan-rules
Extra PHPStan rules by Symplify to catch bugs, improve code quality, and enforce consistent conventions. Easy to install and configure, with a broad set of checks for Symfony/Laravel and modern PHP features to keep your codebase clean.
Set of 70+ PHPStan fun and practical rules that check:
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.
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.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)
{
}
:x:
function run(int $userId)
{
}
:+1:
Interface must be located in "Contract" or "Contracts" namespace
rules:
- Symplify\PHPStanRules\Rules\CheckRequiredInterfaceInContractNamespaceRule
namespace App\Repository;
interface ProductRepositoryInterface
{
}
:x:
namespace App\Contract\Repository;
interface ProductRepositoryInterface
{
}
:+1:
Class should have suffix "%s" to respect parent type
:wrench: configure it!
services:
-
class: Symplify\PHPStanRules\Rules\ClassNameRespectsParentSuffixRule
tags: [phpstan.rules.rule]
arguments:
parentClasses:
- Symfony\Component\Console\Command\Command
↓
class Some extends Command
{
}
:x:
class SomeCommand extends Command
{
}
:+1:
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';
:x:
// correct file path
return __DIR__ . '/../fixtures/some_file.yml';
:+1:
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);
:x:
$items = ['apple', 'banana', 'orange'];
$items = array_map(function ($item) {
return $this->method($item);
}, $items);
:+1:
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()
{
}
}
:x:
final class SomeClass extends ParentClass
{
public function __construct(private string $dependency)
{
}
}
:+1:
Interface have suffix of "Interface", trait have "Trait" suffix exclusively
rules:
- Symplify\PHPStanRules\Rules\Explicit\ExplicitClassPrefixSuffixRule
<?php
interface NotSuffixed
{
}
trait NotSuffixed
{
}
abstract class NotPrefixedClass
{
}
:x:
<?php
interface SuffixedInterface
{
}
trait SuffixedTrait
{
}
abstract class AbstractClass
{
}
:+1:
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"]);
:x:
usort($items, function (array $apples) {
return $this->method($apples);
};
:+1:
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();
}
}
:x:
class SomeClass
{
// ...
public function run()
{
$this->someService->run();
}
}
:+1:
Only abstract classes can be extended
rules:
- Symplify\PHPStanRules\Rules\ForbiddenExtendOfNonAbstractClassRule
final class SomeClass extends ParentClass
{
}
class ParentClass
{
}
:x:
abstract class ParentClass
{
}
:+1:
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);
}
}
:x:
class SomeService
{
public function __construct(private RepositoryService $repositoryService)
{
}
public function run()
{
$item = $this->repositoryService->get(1);
}
}
:+1:
Function "%s()" cannot be used/left in the code
:wrench: 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('...');
:x:
echo '...';
:+1:
Multiple class/interface/trait is not allowed in single file
rules:
- Symplify\PHPStanRules\Rules\ForbiddenMultipleClassLikeInOneFileRule
// src/SomeClass.php
class SomeClass
{
}
interface SomeInterface
{
}
:x:
// src/SomeClass.php
class SomeClass
{
}
// src/SomeInterface.php
interface SomeInterface
{
}
:+1:
"%s" is forbidden to use
:wrench: configure it!
services:
-
class: Symplify\PHPStanRules\Rules\ForbiddenNodeRule
tags: [phpstan.rules.rule]
arguments:
forbiddenNodes:
- PhpParser\Node\Expr\ErrorSuppress
↓
return @strlen('...');
:x:
return strlen('...');
:+1:
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;
}
}
:x:
class SomeClass
{
public function run()
{
return self::SOME_CONST;
}
}
:+1:
Use explicit names over dynamic ones
rules:
- Symplify\PHPStanRules\Rules\NoDynamicNameRule
class SomeClass
{
public function old(): bool
{
return $this->${variable};
}
}
:x:
class SomeClass
{
public function old(): bool
{
return $this->specificMethodName();
}
}
:+1:
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
{
}
:x:
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
class Product
{
}
:+1:
Global constants are forbidden. Use enum-like class list instead
rules:
- Symplify\PHPStanRules\Rules\NoGlobalConstRule
const SOME_GLOBAL_CONST = 'value';
:x:
class SomeClass
{
public function run()
{
return self::SOME_CONST;
}
}
:+1:
Use explicit return value over magic &reference
rules:
- Symplify\PHPStanRules\Rules\NoReferenceRule
class SomeClass
{
public function run(&$value)
{
}
}
:x:
class SomeClass
{
public function run($value)
{
return $value;
}
}
:+1:
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;
}
}
:x:
final class SomeClass
{
private $name;
public function setName(string $name): void
{
$this->name = $name;
}
}
:+1:
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);
}
}
:x:
use PHPUnit\Framework\TestCase;
final class SkipApiMock extends TestCase
{
public function test()
{
$someTypeMock = new class() implements SomeType {};
}
}
:+1:
Instead of "%s" class/interface use "%s"
:wrench: configure it!
services:
-
class: Symplify\PHPStanRules\Rules\PreferredClassRule
tags: [phpstan.rules.rule]
arguments:
oldToPreferredClasses:
SplFileInfo: CustomFileInfo
↓
class SomeClass
{
public function run()
{
return new SplFileInfo('...');
}
}
:x:
class SomeClass
{
public function run()
{
return new CustomFileInfo('...');
}
}
:+1:
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()
{
}
}
:x:
class SomeParentClass
{
public function run()
{
}
}
class SomeClass extends SomeParentClass
{
public function run()
{
}
}
:+1:
@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()
{
}
}
:x:
use Symfony\Component\Routing\Annotation\Route;
class SomeController
{
#[Route(path: "/path")]
public function someAction()
{
}
}
:+1:
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
{
}
:x:
// app/Attribute/SomeAttribute.php
namespace App\Attribute;
#[\Attribute]
final class SomeAttribute
{
}
:+1:
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
{
}
:x:
// app/Exception/SomeException.php
namespace App\Exception;
final class SomeException extends Exception
{
}
:+1:
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';
}
:x:
use MyCLabs\Enum\Enum;
class SomeClass extends Enum
{
private const YES = 'yes';
private const NO = 'no';
}
:+1:
Class "%s" is missing @see annotation with test case class reference
:wrench: configure it!
services:
-
class: Symplify\PHPStanRules\Rules\SeeAnnotationToTestRule
tags: [phpstan.rules.rule]
arguments:
requiredSeeTypes:
- Rule
↓
class SomeClass extends Rule
{
}
:x:
/**
* @see SomeClassTest
*/
class SomeClass extends Rule
{
}
:+1:
Constant "%s" must be uppercase
rules:
- Symplify\PHPStanRules\Rules\UppercaseConstantRule
final class SomeClass
{
public const some = 'value';
}
:x:
final class SomeClass
{
public const SOME = 'value';
}
:+1:
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);
}
}
:x:
class SomeClass
{
public function __construct(SomeEntityRepository $someEntityRepository)
{
}
}
:+1:
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
{
}
:x:
final class SomeRepository
{
public function __construct(EntityManagerInterface $entityManager)
{
$this->repository = $entityManager->getRepository(SomeEntity::class);
}
}
:+1:
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);
}
}
:x:
use Doctrine\ORM\EntityManagerInterface;
final class SomeService
{
public function __construct(private SomeEntityRepository $someEntityRepository)
{
}
}
:+1:
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);
}
}
:x:
use Doctrine\Common\DataFixtures\AbstractFixture;
final class SomeFixture extends AbstractFixture
{
public function load(ObjectManager $objectManager)
{
$someEntity = $this->getReference('some-entity-1');
}
}
:+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
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')]);
};
:x:
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'));
};
:+1:
Prevents using $this->get(...) in commands, to promote dependency injection.
rules:
- Symplify\PHPStanRules\Rules\Symfony\NoGetInCommandRule
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);
};
:x:
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\PreferAutowireAttributeOverConfigParamRule
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeService::class)->args(['%some_param%']);
};
:x:
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class SomeService
{
public function __construct(
#[Autowire(param: 'some_param')]
private string $someParam
) {
}
}
:+1:
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),
]);
};
:x:
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(SomeService::class);
};
:+1:
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
) {
}
}
:x:
abstract class AbstractController extends Controller
{
private $someService;
#[Required]
public function autowireAbstractController(SomeService $someService)
{
$this->someService = $someService;
}
}
:+1:
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);
};
:x:
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']);
};
:+1:
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']);
};
:x:
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']);
};
:+1:
Avoid using configs in *Bundle/Resources directory. Move them to /config directory instead
rules:
- Symplify\PHPStanRules\Rules\Symfony\ConfigClosure\NoBundleResourceConfigRule
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
{
}
:x:
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_USER')]
#[IsGranted('ROLE_ADMIN')]
class SomeController
{
}
:+1:
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
{
}
:x:
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted(SomeEnum::ROLE_USER)]
class SomeController
{
}
:+1:
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');
};
:x:
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
return static function (RoutingConfigurator $routingConfigurator): void {
$routingConfigurator->import(__DIR__ . '/some-path');
};
:+1:
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()
{
}
}
:x:
use Symfony\Component\Routing\Attribute\Route;
class SomeController
{
#[Route('/some-prefix/some-action')]
public function someAction()
{
}
}
:+1:
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)
{
// ...
}
}
:x:
abstract class SomeClass
{
#[Required]
public function autowireSomeClass(SomeService $someService)
{
// ...
}
}
:+1:
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');
}
}
:x:
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
final class SomeClass
{
public function __construct(
private EventDispatcherInterface $eventDispatcher
) {
}
public function run()
{
$this->eventDispatcher->dispatch(new EventObject());
}
}
:+1:
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()
{
}
}
:x:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SomeListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'event' => 'onEvent',
];
}
public function onEvent()
{
}
}
:+1:
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener]
class SomeListener
{
public function __invoke()
{
}
}
:+1:
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)
{
// ...
}
}
:x:
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);
}
}
:+1:
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()
{
}
}
:x:
use Doctrine\Common\EventSubscriber;
use Doctrine\ODM\MongoDB\Events;
class SomeListener implements EventSubscriber
{
public function onFlush()
{
}
public static function getSubscribedEvents(): array
{
return [
Events::onFlush
];
}
}
:+1:
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()
{
}
}
:x:
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class SomeListener implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
Event::class => 'onEvent',
];
}
public function onEvent()
{
}
}
:+1:
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()
{
}
}
:x:
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
final class SomeController extends AbstractController
{
#[Route()]
public function __invoke()
{
}
}
:+1:
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\Doctrine\NoEntityMockingRule
- Symplify\PHPStanRules\Rules\Doctrine\NoDocumentMockingRule
use PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$someEntityMock = $this->createMock(SomeEntity::class);
}
}
:x:
use PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
public function test()
{
$someEntityMock = new SomeEntity();
}
}
:+1:
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);
}
}
:x:
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);
}
}
:+1:
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 [];
}
}
:x:
use PHPUnit\Framework\TestCase;
final class SomeTest extends TestCase
{
/**
* @dataProvider dataProvider
*/
public function test(): array
{
return [];
}
public static function dataProvider(): array
{
return [];
}
}
:+1:
Happy coding!
How can I help you explore Laravel packages today?