gian-tiaga / phpstan-strict-rules
Strict PHPStan rules for PHP applications
Requires
- php: >=8.5
- phpstan/phpstan: ^2.1.54
Requires (Dev)
- php-cs-fixer/shim: ^3.95.23
- phpunit/phpunit: ^13.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
gian-tiaga/phpstan-strict-rules — набор строгих PHPStan-правил для PHP-кода.
Сообщения правил остаются на английском языке. Это developer-инструмент, а не пользовательский интерфейс приложения.
Установка
composer require --dev gian-tiaga/phpstan-strict-rules:^0.1.0
Подключите extension в phpstan.neon:
includes: - vendor/gian-tiaga/phpstan-strict-rules/extension.neon
Настраиваются интерфейс разрешённой массовой записи, имена атрибутов доступа к маршруту и интерфейс интеграционного события.
Правила
declare(strict_types=1)
Каждый анализируемый PHP-файл должен начинаться с declare(strict_types=1).
Идентификатор:
gianTiaga.phpstanStrictRules.missingStrictTypes.
Запрещено:
<?php namespace App\Example;
Разрешено:
<?php declare(strict_types=1); namespace App\Example;
Именованные аргументы
Если в вызове два или больше обычных аргумента, они должны быть именованными.
Идентификатор:
gianTiaga.phpstanStrictRules.namedArgumentsRequired.
Запрещено:
$user = new User('email@example.com', 'Иван'); $logger->info('message', ['userId' => $userId]);
Разрешено:
$user = new User(email: 'email@example.com', name: 'Иван'); $logger->info(message: 'message', context: ['userId' => $userId]);
Один позиционный аргумент разрешён. Вызовы PHPUnit assertions не проверяются этим правилом, потому что PHPUnit запрещает named arguments для своих assert-методов.
Строгие сравнения
Loose comparison запрещён.
Идентификаторы:
gianTiaga.phpstanStrictRules.looseEqualForbidden;gianTiaga.phpstanStrictRules.looseNotEqualForbidden.
Запрещено:
if ($status == 'active') { } if ($status != 'active') { }
Разрешено:
if ($status === UserStatus::Active) { } if ($status !== UserStatus::Active) { }
Точные PHPDoc-типы
PHPDoc-контракты не должны содержать неявный mixed, вложенные массивы и array shape.
Идентификаторы:
gianTiaga.phpstanStrictRules.noImplicitMixedType;gianTiaga.phpstanStrictRules.noNestedArrayType;gianTiaga.phpstanStrictRules.noArrayShapeType.
Запрещено:
/** * @param array $items * @return array */ public function names(array $items): array { return $items; }
Разрешено:
/** * @param list<UserName> $items * @return list<string> */ public function names(array $items): array { return array_map( callback: static fn(UserName $name): string => $name->value, array: $items, ); }
Если данные сложные, лучше вынести их в DTO или именованную коллекцию, а не описывать многоуровневым массивом.
Set-based запись
insert(), update() и delete() на Cycle\Database\DatabaseInterface запрещены везде, кроме классов, реализующих интерфейс-маркер санкционированной записи. Обычное изменение состояния идёт через сущность и EntityManager, поэтому массовая запись остаётся узкой осознанной дверью. EntityManager::delete($entity) под правило не попадает: у него другой тип получателя.
Идентификатор:
gianTiaga.phpstanStrictRules.setBasedWriteForbidden.
Запрещено:
final class MarkNotificationsRead { public function __construct( private readonly DatabaseInterface $database, ) {} public function run(): void { $this->database->update('notifications'); } }
Разрешено:
final class MarkNotificationsRead implements SetBasedWrite { public function __construct( private readonly DatabaseInterface $database, ) {} public function run(): void { $this->database->update('notifications'); } }
Параметр setBasedWriteMarkerInterface
Имя интерфейса-маркера задаётся параметром конфигурации: пакет правил не знает классов конкретного приложения, поэтому свой маркер объявляет каждый проект и каждый пакет.
parameters: setBasedWriteMarkerInterface: App\Shared\Infrastructure\Database\SetBasedWrite
Значение по умолчанию — пустая строка. Она не освобождает ни один класс: пока маркер не задан, set-based запись запрещена везде, и сообщение правила называет сам параметр. Так забытая настройка проверку не ослабляет.
Маркер должен быть автозагружаемым интерфейсом: правило проверяет его через рефлексию, а не по тексту.
Объявление доступа к маршруту
Метод с атрибутом Spiral Route обязан иметь ровно один атрибут из списка
routeAccessAttributeClasses. Методы без Route правило не проверяет.
Идентификатор:
gianTiaga.phpstanStrictRules.singleRouteAccessAttributeRequired.
parameters: routeAccessAttributeClasses: - App\Shared\Infrastructure\Http\Attribute\PublicRoute - App\Modules\Identity\Public\Attribute\AuthenticatedRoute - App\Modules\Identity\Public\Attribute\RequiresPermission
Список по умолчанию пуст, и тогда правило отключено. Пакет не знает атрибуты приложения и получает их только из его конфигурации.
Форма интеграционного события
Класс, реализующий интерфейс интеграционного события, обязан собираться обратно из своей полезной нагрузки. Правило рекурсивно обходит свойства самого события и свойства вложенных объектов и требует, чтобы у каждого свойства был одноимённый параметр конструктора, а его тип был описан.
Допустимы string, int, float, bool, null, backed enum, DateTimeImmutable, вложенный объект по тем же правилам, list<T> и array<string, T> с указанным T. Видимость свойства не ограничена; статическое свойство полем события не является.
Идентификаторы:
gianTiaga.phpstanStrictRules.integrationEventFieldTypeForbidden;gianTiaga.phpstanStrictRules.integrationEventConstructorParameterRequired.
Запрещено:
final class StockChangedEvent implements IntegrationEventContract { public string $traceId = 'trace-1'; public function __construct( public array $rows, public mixed $anything, public object $payload, ) {} }
Разрешено:
final readonly class StockChangedEvent implements IntegrationEventContract { /** @param list<StockLine> $rows */ public function __construct( public string $clientId, public StockReason $reason, public \DateTimeImmutable $occurredAt, public array $rows, ) {} }
Параметр integrationEventInterface
Имя интерфейса события задаётся параметром конфигурации: пакет правил не знает классов конкретного приложения.
parameters: integrationEventInterface: GianTiaga\SpiralOutbox\IntegrationEventContract
Значение по умолчанию — пустая строка: пока интерфейс не задан, правило не проверяет ни один класс. Интерфейс должен быть автозагружаемым — правило работает через рефлексию, а не по тексту.
Локальная разработка
composer install
composer test
composer phpstan
Если пакет подключён через path repository и вы меняете сами правила, иногда нужно очистить кеш PHPStan:
vendor/bin/phpstan clear-result-cache