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