Larastan позволяет найти ошибки в вашем Laravel-приложении еще до его запуска. Он представляет собой обертку PHPStan, предназначенную специально для статического анализа с поддержки всей магии внутри Laravel.
В этой статье я намерен провести вас через все этапы от установки Larastan до достижения 9 уровня, не игнорируя абсолютно никаких правил.
Из README Larastan следует, что для его установки нужно сделать следующее:
Запустить
composer require larastan/larastan:^2.0 --devДобавить файл
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 на каждом уровне:
базовые проверки, неизвестные классы, неизвестные функции, неизвестные методы, вызываемые из
$this, неправильное количество аргументов, передаваемых этим методам и функциям, никогда неопределенные переменныепотенциально неопределенные переменные, неизвестные магические методы и свойства классов с
__callи__getпроверка неизвестных методов во всех выражениях (не только для
$this), проверка PHPDocsвозвращаемые типы, типы, назначаемые свойствам
базовая проверка мертвого кода — всегда ложные проверки
instanceofи других типов, мертвые ветвиelse, недостижимый код послеreturn; и т.д.проверка типов аргументов, передаваемых методам и функциям
отчет об отсутствующих аннотациях типов
отчет о частично неправильных типах объединений — если вы вызываете метод, который существует только для некоторых типов в типе объединения, уровень 7 отрапортует об этом; другие возможные неправильные ситуации
отчет о вызове методов и доступе к свойствам для типов с нулевым значением
более строгое отношение к типу
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, чтобы показать вам два способа устранения одной и той же ошибки:
Используя
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; }
Используя
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 можно изучить на онлайн-курсе по руководством экспертов.
