Set of rules for PHPStan used by Symplify projects
- See Rules Overview
composer require symplify/phpstan-rules --dev
Note: Make sure you use phpstan/extension-installer
to load necessary service configs.
Sets are bunch of rules grouped by a common area, e.g. improve naming. You can pick from 5 sets:
includes:
- vendor/symplify/phpstan-rules/config/code-complexity-rules.neon
- vendor/symplify/phpstan-rules/config/naming-rules.neon
- vendor/symplify/phpstan-rules/config/regex-rules.neon
- vendor/symplify/phpstan-rules/config/static-rules.neon
Add sets one by one, fix what you find useful and ignore the rest.
Do you write custom Rector rules? Add rules for them too:
includes:
- vendor/symplify/phpstan-rules/config/rector-rules.neon
There is one set with pre-configured configurable rules. Include it and see what is errors are found:
# phpstan.neon
includes:
- vendor/symplify/phpstan-rules/config/configurable-rules.neon
Would you like to tailor it to fit your taste? Pick one PHPStan rule and configure it manually β
services:
-
class: Symplify\PHPStanRules\Rules\ForbiddenNodeRule
tags: [phpstan.rules.rule]
arguments:
forbiddenNodes:
- PhpParser\Node\Expr\Empty_
- PhpParser\Node\Stmt\Switch_
Add regex101.com link to that shows the regex in practise, so it will be easier to maintain in case of bug/extension in the future
class SomeClass
{
private const COMPLICATED_REGEX = '#some_complicated_stu|ff#';
}
β
class SomeClass
{
/**
* @see https://regex101.com/r/SZr0X5/12
*/
private const COMPLICATED_REGEX = '#some_complicated_stu|ff#';
}
π
Class like namespace "%s" does not follow PSR-4 configuration in composer.json
// defined "Foo\Bar" namespace in composer.json > autoload > psr-4
namespace Foo;
class Baz
{
}
β
// defined "Foo\Bar" namespace in composer.json > autoload > psr-4
namespace Foo\Bar;
class Baz
{
}
π
Interface must be located in "Contract" or "Contracts" namespace
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
{
}
π
Interface have suffix of "Interface", trait have "Trait" suffix exclusively
<?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
usort($items, [$this, "method"]);
β
usort($items, function (array $apples) {
return $this->method($apples);
};
π
Only abstract classes can be extended
final class SomeClass extends ParentClass
{
}
class ParentClass
{
}
β
final class SomeClass extends ParentClass
{
}
abstract class ParentClass
{
}
π
Function "%s()"
cannot be used/left in the code
π§ configure it!
services:
-
class: Symplify\PHPStanRules\Rules\ForbiddenFuncCallRule
tags: [phpstan.rules.rule]
arguments:
forbiddenFunctions:
- eval
β
echo eval('...');
β
echo '...';
π
services:
-
class: Symplify\PHPStanRules\Rules\ForbiddenFuncCallRule
tags: [phpstan.rules.rule]
arguments:
forbiddenFunctions:
dump: 'seems you missed some debugging function'
β
dump($value);
echo $value;
β
echo $value;
π
Multiple class/interface/trait is not allowed in single file
// 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
class SomeClass
{
public function run()
{
return static::SOME_CONST;
}
}
β
class SomeClass
{
public function run()
{
return self::SOME_CONST;
}
}
π
Use explicit names over dynamic ones
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
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
const SOME_GLOBAL_CONST = 'value';
β
class SomeClass
{
public function run()
{
return self::SOME_CONST;
}
}
π
Use local named constant instead of inline string for regex to explain meaning by constant name
class SomeClass
{
public function run($value)
{
return preg_match('#some_stu|ff#', $value);
}
}
β
class SomeClass
{
/**
* @var string
*/
public const SOME_STUFF_REGEX = '#some_stu|ff#';
public function run($value)
{
return preg_match(self::SOME_STUFF_REGEX, $value);
}
}
π
Use explicit return value over magic &reference
class SomeClass
{
public function run(&$value)
{
}
}
β
class SomeClass
{
public function run($value)
{
return $value;
}
}
π
Use value object over return of values
class ReturnVariables
{
public function run($value, $value2): array
{
return [$value, $value2];
}
}
β
final class ReturnVariables
{
public function run($value, $value2): ValueObject
{
return new ValueObject($value, $value2);
}
}
π
Setter method cannot return anything, only set value
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;
}
}
π
Interface "%s" has only single implementer. Consider using the class directly as there is no point in using the interface.
class SomeClass implements SomeInterface
{
}
interface SomeInterface
{
}
β
class SomeClass implements SomeInterface
{
}
class AnotherClass implements SomeInterface
{
}
interface SomeInterface
{
}
π
Mocking "%s" class is forbidden. Use direct/anonymous class instead for better static analysis
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.
class SomeParentClass
{
public function run()
{
}
}
class SomeClass extends SomeParentClass
{
protected function run()
{
}
}
β
class SomeParentClass
{
public function run()
{
}
}
class SomeClass extends SomeParentClass
{
public function run()
{
}
}
π
Name your constant with "_REGEX" suffix, instead of "%s"
class SomeClass
{
public const SOME_NAME = '#some\s+name#';
public function run($value)
{
$somePath = preg_match(self::SOME_NAME, $value);
}
}
β
class SomeClass
{
public const SOME_NAME_REGEX = '#some\s+name#';
public function run($value)
{
$somePath = preg_match(self::SOME_NAME_REGEX, $value);
}
}
π
Attribute must have all names explicitly defined
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
// 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
// app/Controller/SomeException.php
namespace App\Controller;
final class SomeException extends Exception
{
}
β
// app/Exception/SomeException.php
namespace App\Exception;
final class SomeException extends Exception
{
}
π
Use invokable controller with __invoke()
method instead of named action method
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;
use Symfony\Component\Routing\Annotation\Route;
final class SomeController extends AbstractController
{
#[Route()]
public function __invoke()
{
}
}
π
Enum constants "%s" are duplicated. Make them unique instead
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
final class SomeClass
{
public const some = 'value';
}
β
final class SomeClass
{
public const SOME = 'value';
}
π
Happy coding!