Search by

gian-tiaga / phpstan-strict-rules

gian_tiaga

Strict PHPStan rules for PHP applications

Package info

github.com/falur/phpstan-strict-rules

pkg:composer/gian-tiaga/phpstan-strict-rules

Statistics

Installs: 7

Dependents: 4

Suggesters: 0

Stars: 0

Open Issues: 0

v0.1.0 2026-09-14 14:35 UTC

This package is auto-updated.

Last update: 2026-09-14 14:48:11 UTC


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