Larastan позволяет найти ошибки в вашем Laravel-приложении еще до его запуска. Он представляет собой обертку PHPStan, предназначенную специально для статического анализа с поддержки всей магии внутри Laravel.

В этой статье я намерен провести вас через все этапы от установки Larastan до достижения 9 уровня, не игнорируя абсолютно никаких правил.

Из README Larastan следует, что для его установки нужно сделать следующее:

  1. Запустить composer require larastan/larastan:^2.0 --dev

  2. Добавить файл phpstan.neon или phpstan.neon.dist в корневой каталог вашего проекта:

includes:
    - vendor/larastan/larastan/extension.neon

parameters:

    paths:
        - app/

    # Level 9 is the highest level
    level: 5

Как вы видите, по умолчанию он настроен 5-й уровень на проверки, но мы изменим его уровень на 0.

Прежде чем продолжить, нам необходимо разобраться, что проверяется Larastan на каждом уровне:

  1. базовые проверки, неизвестные классы, неизвестные функции, неизвестные методы, вызываемые из $this, неправильное количество аргументов, передаваемых этим методам и функциям, никогда неопределенные переменные

  2. потенциально неопределенные переменные, неизвестные магические методы и свойства классов с __call и __get

  3. проверка неизвестных методов во всех выражениях (не только для $this), проверка PHPDocs

  4. возвращаемые типы, типы, назначаемые свойствам

  5. базовая проверка мертвого кода — всегда ложные проверки instanceof и других типов, мертвые ветви else, недостижимый код после return; и т.д.

  6. проверка типов аргументов, передаваемых методам и функциям

  7. отчет об отсутствующих аннотациях типов

  8. отчет о частично неправильных типах объединений — если вы вызываете метод, который существует только для некоторых типов в типе объединения, уровень 7 отрапортует об этом; другие возможные неправильные ситуации

  9. отчет о вызове методов и доступе к свойствам для типов с нулевым значением

  10. более строгое отношение к типу mixed — единственная разрешенная операция, которую вы можете сделать с ним, это передать его другому mixed

Учитывая эти правила, предположим, что у нас есть следующий код (для простоты все находится в одном файле):

Примечание: Приведенный здесь код специально был написан так, чтобы я мог получить ошибки, необходимые для того, чтобы показать вам, как действовать дальше, но некоторые его части могут быть написаны более простыми способами.

declare(strict_types=1);

use App\Http\Controllers\Controller;
use App\Models\Appointment;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;

class User extends Model
{
    public function appointments()
    {
        return $this->hasMany(Appointment::class);
    }
}

class UserDTO
{
    public function __construct(
        public $name,
        public $is_active,
    ) {
    }

    public function toArray()
    {
        return [
            'name' => $this->name,
            'is_active' => $this->is_active,
        ];
    }
}

class ShowUserQuery
{
    public function run($id)
    {
        $this->doSomething();

        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

class UserController extends Controller
{
    public function show($id, ShowUserQuery $query)
    {
        return response()->json($query->run($id)->toArray());
    }

    public function store(Request $request)
    {
        $request->validate([
            'name' => ['required', 'max:250'],
            'is_active' => ['required', 'boolean'],
        ]);

        if (true) {
            return;
        }

        $isActive = $request->input('is_active');

        $userDTO = new UserDTO(
            $request->input('name'),
            $isActive
        );

        $user = User::create($userDTO->toArray());

        return $user;
    }
}

После запуска ./vendor/bin/phpstan analyze мы получаем следующие ошибки:

 22     Call to an undefined method ShowUserQuery::doSomething().  
  24     Relation 'appointments' is not found in User model. 

Чтобы исправить их, нам нужно удалить или определить неопределенный метод и добавить возвращаемый тип для отношения модели, в результате чего мы получим следующее:

class User extends Model
{
    public function appointments(): HasMany
    {
        return $this->hasMany(Appointment::class);
    }
}

class ShowUserQuery
{
    public function run($id)
    {
        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

До 4-го уровня мы не получаем никаких ошибок, потому что в этом случае мы не нарушаем правила уровней 1, 2 и 3. После повышения до уровня 4 мы получаем следующие ошибки:

 43     If condition is always true.                           
  47     Unreachable statement - code above always terminates.

Чтобы решить эту проблему, нам нужно удалить оператор if, расположенный внутри метода store нашего контроллера. После исправления функция приобретет следующий вид:

public function store(Request $request)
{
    $data = $request->validate([
        'name' => ['required', 'max:250'],
        'is_active' => ['required', 'boolean'],
    ]);

    $isActive = $request->input('is_active');

    $userDTO = new UserDTO(
        $request->input('name'),
        $isActive
    );

    $user = User::create($userDTO->toArray());

    return $user;
}

Относительно 5-го уровня все в порядке, но на 6-м уровне мы получаем кучу ошибок:

 13     Method User::appointments() return type with generic class                  
         Illuminate\Database\Eloquent\Relations\HasMany does not specify its types:  
         TRelatedModel                                                               
         ? You can turn this off by setting                                         
            checkGenericClassInNonGenericObjectType: false in your                   
            phpstan.neon.                                                            
  21     Method ShowUserQuery::run() has no return type specified.                   
  21     Method ShowUserQuery::run() has parameter $id with no type specified.       
  31     Method UserController::show() has no return type specified.                 
  31     Method UserController::show() has parameter $id with no type specified.     
  36     Method UserController::store() has no return type specified. 

Что ж, приступим к исправлению проблем. На этом уровне нам нужно указать типы возвратов и параметров для всего кода. В первой ошибке нам предлагается указать тип связанной модели в определении отношения.

Исправив все проблемы, мы получили такой код:

class User extends Model
{
    /**
     * @return HasMany<Appointment>
     */
    public function appointments(): HasMany
    {
        return $this->hasMany(Appointment::class);
    }
}

class UserDTO
{
    public function __construct(
        public string $name,
        public bool $is_active,
    ) {
    }

    /**
     * @return array{name: string, is_active: bool}
     */
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'is_active' => $this->is_active,
        ];
    }
}

class ShowUserQuery
{
    public function run(int $id): ?User
    {
        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

class UserController extends Controller
{
    public function show(int $id, ShowUserQuery $query): JsonResponse
    {
        return response()->json($query->run($id)->toArray());
    }

    public function store(Request $request): User
    {
        $request->validate([
            'name' => ['required', 'max:250'],
            'active' => ['required', 'boolean'],
        ]);
        $isActive = $request->input('is_active');

        $userDTO = new UserDTO(
            $request->input('name'),
            $isActive
        );

        $user = User::create($userDTO->toArray());

        return $user;
    }
}

Относительно 7-го уровня все понятно, а вот касательно 8-го, к сожалению, нет:

37     Cannot call method toArray() on User|null.  

Чтобы исправить это, мы должны избегать вызова методов или свойств в обнуляемых типах. Наше исправление будет таким:

public function show(int $id, ShowUserQuery $query): JsonResponse
{
    $userArray = $query->run($id)?->toArray() ?? [];

    return response()->json($userArray);
}

Наконец, мы доходим до максимального и самого ограничительного уровня, а именно уровня 9, и здесь возникают ошибки, связанные с mixed значениями. Для этого сценария я специально сделал переменную $isActive, чтобы показать вам два способа устранения одной и той же ошибки:

  1. Используя assert и string методы запроса:

public function store(Request $request): User
{
    $request->validate([
        'name' => ['required', 'max:250'],
        'active' => ['required', 'boolean'],
    ]);
    $isActive = $request->input('is_active');
    assert(is_bool($isActive));

    $userDTO = new UserDTO(
        $request->string('name')->toString(),
        $isActive
    );

    $user = User::create($userDTO->toArray());

    return $user;
}
  1. Используя string и boolean методы в запросе:

public function store(Request $request): User
{
    $request->validate([
        'name' => ['required', 'max:250'],
        'active' => ['required', 'boolean'],
    ]);

    $userDTO = new UserDTO(
        $request->string('name')->toString(),
        $request->boolean('is_active')
    );

    $user = User::create($userDTO->toArray());

    return $user;
}

После выполнения всех исправлений с 0 по 9 уровень Larastan/PHPStan наш финальный код будет выглядеть так:

<?php

declare(strict_types=1);

use App\Http\Controllers\Controller;
use App\Models\Appointment;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;

class User extends Model
{
    /**
     * @return HasMany<Appointment>
     */
    public function appointments(): HasMany
    {
        return $this->hasMany(Appointment::class);
    }
}

class UserDTO
{
    public function __construct(
        public string $name,
        public bool $is_active,
    ) {
    }

    /**
     * @return array{name: string, is_active: bool}
     */
    public function toArray(): array
    {
        return [
            'name' => $this->name,
            'is_active' => $this->is_active,
        ];
    }
}

class ShowUserQuery
{
    public function run(int $id): ?User
    {
        return User::query()
            ->with('appointments')
            ->find($id);
    }
}

class UserController extends Controller
{
    public function show(int $id, ShowUserQuery $query): JsonResponse
    {
        $userArray = $query->run($id)?->toArray() ?? [];

        return response()->json($userArray);
    }

    public function store(Request $request): User
    {
        $request->validate([
            'name' => ['required', 'max:250'],
            'active' => ['required', 'boolean'],
        ]);

        $userDTO = new UserDTO(
            $request->string('name')->toString(),
            $request->boolean('is_active')
        );

        $user = User::create($userDTO->toArray());

        return $user;
    }
}

Надеюсь, этот пример поможет вам понять, как с помощью Larastan сделать так, чтобы ваше приложение не содержало банальных ошибок еще до того, как оно будет запущено, и пройти весь путь до 9-го уровня.


Все актуальные инструменты и методы создания эффективных приложений на Laravel можно изучить на онлайн-курсе по руководством экспертов.

Комментарии (0)