Как поднять уровень анализа вашего Laravel-приложения с 0 до 9 в Larastan

от автора

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 можно изучить на онлайн-курсе по руководством экспертов.


ссылка на оригинал статьи https://habr.com/ru/articles/816873/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *