<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
ТЕСТУВАННЯ · ЯКІСТЬ КОДУ

PHPStan і рівніPHPStan levels

PHPStan читає код і знаходить помилки, які інакше вилізли б на продакшні з першим невдалим запитом. Рівні від 0 до 10 вирішують, наскільки він прискіпливий, а baseline дає підняти рівень у старому проєкті за один день.

10 хв читання Junior → Middle
01 · ЩО ЦЕ

Помилки, знайдені без запуску

Метод find() повертає ?User, а контролер одразу читає $user->name. Тести проходять, бо в них користувач завжди є. На продакшні хтось відкриває профіль за старим посиланням, і сторінка падає з TypeError. PHPStan бачить цей рядок, не запускаючи жодного: він виводить з сигнатур і PHPDoc і перевіряє кожен вираз.

СЛОВНИКВиведення типів. PHPStan знає тип кожного виразу: з типів параметрів, PHPDoc, повернень функцій і перевірок вище в коді. Після if ($user === null) { return; } $user уже User.

каже, які перевірки ввімкнено, і кожен додає свої до попередніх. 0–2: невідомі класи, методи й змінні, коректність PHPDoc. 3–5: типи повернення й властивостей, мертві гілки, типи аргументів. 6: тип має бути скрізь, а масиви потребують на кшталт list<Job> або . 7–8: частково хибні union-типи й звертання до nullable без перевірки. 9–10: mixed стає суворим, спершу явний, потім і неявний. Повзунок на головній сторінці розділу показує це на одному контролері.

СЛОВНИКРівень. Число від 0 до 10 у phpstan.neon або --level. Рівень N вмикає всі перевірки рівнів 0..N. level: max дорівнює найвищому.
СЛОВНИКGenerics у PHPDoc. Типи з параметром, яких PHP сам не має: list<Job>, array<string, int>, Collection<int, User>. Свої класи оголошують через @template T.
СЛОВНИКArray shape. Форма масиву в PHPDoc: array{from: int, to: int, currency: string}. PHPStan перевіряє ключі й типи значень, тож одруківка в ключі стає помилкою.

Старий проєкт на рівні 8 видасть сотні помилок. записує їх у файл, і далі PHPStan падає лише на нових. Окреме місце, де ви знаєте більше за аналізатор, глушать . Магію Laravel PHPStan сам не розуміє: властивості моделей, фасади, query builder пояснює йому розширення .

СЛОВНИКBaseline. Файл phpstan-baseline.neon зі списком наявних помилок: повідомлення, ідентифікатор, файл і скільки разів. Створюється командою --generate-baseline і підключається в phpstan.neon.
СЛОВНИК@phpstan-ignore. Коментар над рядком або в ньому: // @phpstan-ignore property.nonObject. Глушить лише помилку з цим ідентифікатором і сам стає помилкою, якщо такої там немає.
СЛОВНИКLarastan. Розширення PHPStan для Laravel. Читає міграції, щоб знати колонки моделей, розуміє фасади, query builder і відношення, позначені generic-типом.
02 · ВАРІАНТИ

Одна помилка, пʼять способів з нею жити

Рівень 8 каже про ProfileController::show(): Cannot access property $name on User|null. [identifier=property.nonObject]. Перемикайте вкладки: від виправлення до того, як цю помилку відкладають.

php ProfileController.php Виправити
1public function show(int $id): string
2{
3 $user = $this->users->find($id)
4 ?? throw new DomainException("Користувача {$id} немає");
5
6 return $user->name;
7}
ЩО ПЕРЕВІРЯЄ
Після ?? throw PHPStan знає, що $user уже User, і помилки немає. Сторінка для неіснуючого id тепер падає зрозумілим винятком, який можна перетворити на 404, а не TypeError з глибини.
КОЛИ БРАТИ
Завжди, коли помилка справжня й виправлення вміщується в поточну зміну. Це більшість випадків.
php ProfileController.php @phpstan-ignore
1public function show(int $id): string
2{
3 $user = $this->users->find($id);
4
5 // @phpstan-ignore property.nonObject (id приходить з маршруту, користувач точно є)
6 return $user->name;
7}
ЩО ПЕРЕВІРЯЄ
Глушиться рівно одна помилка з ідентифікатором property.nonObject на наступному рядку. Пояснення в дужках PHPStan пропускає. Якщо ідентифікатор неправильний, помилок дві: стара лишається, а ще No error with identifier argument.type is reported on line N.
КОЛИ БРАТИ
Ви справді знаєте більше за аналізатор, і це можна пояснити одним реченням. Голий @phpstan-ignore без ідентифікатора PHPStan 2 не приймає: Unexpected end after @phpstan-ignore, expected identifier.
php ProfileController.php ignore-line
1public function show(int $id): string
2{
3 $user = $this->users->find($id);
4
5 return $user->name; // @phpstan-ignore-line
6}
ЩО ПЕРЕВІРЯЄ
Нічого. Рядок мовчить про все: про nullable сьогодні й про одруківку в імені властивості, яку хтось додасть завтра.
КОЛИ БРАТИ
Майже ніколи. Лишається зі старого коду; замінюйте на @phpstan-ignore з ідентифікатором, коли торкаєтеся рядка.
php ProfileController.php ignoreErrors
1# phpstan.neon
2parameters:
3 level: 8
4 ignoreErrors:
5 -
6 identifier: property.nonObject
7 path: app/Http/Controllers/ProfileController.php
ЩО ПЕРЕВІРЯЄ
Усі помилки property.nonObject у цьому файлі, хоч одна, хоч двадцять, у будь-якому методі. Коду контролера правило не видно.
КОЛИ БРАТИ
Цілий файл чи папка свідомо поза правилом, наприклад згенерований код. Без path таке правило глушить ідентифікатор у всьому проєкті.
php ProfileController.php Baseline
1# phpstan-baseline.neon, згенеровано: vendor/bin/phpstan analyse --generate-baseline
2parameters:
3 ignoreErrors:
4 -
5 message: '#^Cannot access property \$name on User\|null\.$#'
6 identifier: property.nonObject
7 count: 1
8 path: app/Http/Controllers/ProfileController.php
ЩО ПЕРЕВІРЯЄ
Точне повідомлення, файл і кількість. Друга така сама помилка в цьому файлі вже не сховається: count 1. А коли першу виправлять, PHPStan сам скаже: Ignored error pattern ... was not matched in reported errors, і рядок треба прибрати з baseline.
КОЛИ БРАТИ
Борг, який зараз не виправити: новий рівень у старому проєкті. Файл підключають у phpstan.neon через includes і скорочують, а не поповнюють.
03 · КОД

Що буває без PHPStan

PHP 8.5
1<?php
2
3declare(strict_types=1);
4
5final class User
6{
7 public function __construct(public string $name) {}
8}
9
101final class Users
11{
12 public function find(int $id): ?User
13 {
14 return $id === 42 ? new User('Олена') : null;
15 }
16}
17
18final class ProfileController
19{
20 public function __construct(private Users $users) {}
21
222 // PHPStan, рівень 8: Cannot access property $name on User|null.
23 public function show(int $id): string
24 {
25 $user = $this->users->find($id);
26
27 return $user->name;
28 }
29
303 // PHPStan, рівень 9: Cannot access offset 'title' on mixed.
31 public function title(string $raw): string
32 {
33 $data = json_decode($raw, true);
34
35 return $data['title'];
36 }
37}
38
394// Попередження PHP теж друкуємо, щоб їх було видно у виводі.
40set_error_handler(function (int $level, string $message): bool {
41 echo " warning: {$message}\n";
42
43 return true;
44});
45
46/** @param callable(): string $call */
47function attempt(string $label, callable $call): void
48{
49 echo $label, "\n";
50
51 try {
52 $result = $call();
53 echo ' ✓ ', $result, "\n";
54 } catch (TypeError $e) {
55 echo ' ✗ TypeError: ', $e->getMessage(), "\n";
56 }
57}
58
595$controller = new ProfileController(new Users());
60
61attempt('show(42), користувач є:', fn () => $controller->show(42));
62attempt('show(7), користувача немає:', fn () => $controller->show(7));
63attempt('title() з правильним JSON:', fn () => $controller->title('{"title": "Senior PHP"}'));
64attempt('title() з одруківкою в ключі:', fn () => $controller->title('{"titel": "Senior PHP"}'));
65attempt('title() з числом замість обʼєкта:', fn () => $controller->title('42'));
66
67echo "\nОбидва методи PHPStan позначив би до першого запуску, без жодного з цих вхідних даних.\n";
Перший запуск завантажить рушій PHP 8.5, приблизно 3,8 МБ Відкрити в пісочниці ↗
ВИВІД
Завантажуємо рушій PHP 8.5
Лише перший раз. Далі код запускається миттєво, рушій лишається в кеші браузера.
$ php main.php ▍
  1. 1 рядки 10–16

    find() чесно обіцяє ?User: користувача може не бути. PHPStan вірить цьому типу більше, ніж тестам, де користувач є завжди.

  2. 2 рядки 22–28

    Рівень 8: Cannot access property $name on User|null. Без PHPStan про це скаже лише запит з неіснуючим id.

  3. 3 рядки 30–36

    Рівень 9: json_decode() повертає mixed, і брати з нього title не можна, доки тип не звужено. Рівень 8 цього ще не бачить.

  4. 4 рядки 39–57

    Обвʼязка: попередження PHP друкуються у вивід, TypeError перехоплюється. PHPDoc callable(): string тримає цей код чистим навіть на рівні 10.

  5. 5 рядки 59–67

    Два вдалі виклики і три невдалі. Кожен невдалий PHP помічає лише в момент виконання, і лише для цього вхідного значення.

  6. Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
04 · ЯК У PHP UKRAINE

PHPStan рівня 8 перед кожним деплоєм

Конфіг PHP Ukraine короткий: Larastan, рівень 8, папки app/ і src/. Baseline немає, тож нова помилка на цьому рівні одразу червона. deploy/deploy.sh першим кроком запускає vendor/bin/phpstan analyse --no-progress --memory-limit=1G і зупиняє деплой, якщо є хоч одна помилка; тести йдуть після нього. На весь код є один @phpstan-ignore argument.type у EloquentSearchEngine, і рядком вище пояснено, чому сирий SQL там безпечний.

php phpstan.neon read-only
1includes:
2 - vendor/larastan/larastan/extension.neon← Larastan
3
4parameters:
5 level: 8← рівень 8
6 paths:
7 - app← лише власний код
8 - src
9 tmpDir: storage/phpstan
05 · ТИПОВІ ПОМИЛКИ

Як PHPStan перестає допомагати

✕ 1

Одразу на рівень 9

Команда вмикає високий рівень у проєкті, який ніколи не аналізували, отримує дві тисячі помилок і вимикає PHPStan на тиждень, тобто назавжди.

Натомість: піднімайте по одному рівню: --level наступного, --generate-baseline, коміт. Нові помилки ловляться з першого дня, а baseline зменшується разом з рефакторингом.

✕ 2

Baseline як смітник

Кожну нову помилку дописують у baseline, бо «так швидше». Через рік у ньому тисяча рядків, і рівень у phpstan.neon більше нічого не означає.

Натомість: перегенеровуйте baseline лише разом з підняттям рівня. У рев'ю зростання phpstan-baseline.neon має бути так само помітним, як вимкнений тест.

✕ 3

Глухі ігнори

// @phpstan-ignore-line або ignoreErrors з регулярним виразом без path. Такий ігнор ховає й ту помилку, яку додадуть у цей рядок чи файл завтра.

Натомість: @phpstan-ignore з ідентифікатором і причиною в дужках. Якщо помилки з цим ідентифікатором там немає, PHPStan про це скаже, і зайвий ігнор не залишиться.

✕ 4

array без форми

Рівень 6 вимагає тип значень масиву, і його закривають @param array<mixed>. Тоді $salary['curency'] з одруківкою проходить, а на рантаймі повертає null.

Натомість: опишіть форму: @param array{from: int, to: int, currency: string} $salary. PHPStan відповість Offset 'curency' does not exist on array{from: int, to: int, currency: string}. Масив, що ходить між шарами, частіше варто перетворити на value object.

06 · ДЕРЕВО РІШЕНЬ

Що робити з новою помилкою PHPStan

1
Помилка в коді, який ви зараз пишете чи змінюєте?
Новий клас або метод, який ви щойно відредагували, а не легасі, якого ви не торкалися.
НІ →Це старий борг, який показав новий рівень чи нова версія PHPStan. Згенеруйте baseline, закомітьте його окремо й повертайтеся до цих помилок з рефакторингом.
2
PHPStan бачить справжню проблему?
null, що справді може прийти; mixed з json_decode(); тип аргументу, що не збігається.
НІ →Спершу дайте йому тип: generic, array shape, @return у PHPDoc. Якщо й так не виходить, @phpstan-ignore з ідентифікатором і причиною.
3
Виправлення вміщується в цю зміну?
Перевірка на null, тип параметра, форма масиву, ?? throw.
НІ →Окрема задача й рядок у baseline. Ігнор у коді для боргу гірший: його ніхто не шукатиме.
ТАК НА ВСІ →Виправте одразу. Дешевшого моменту не буде.
07 · СЛОВНИК
Виведення типів
PHPStan знає тип кожного виразу: з типів параметрів, PHPDoc, повернень функцій і перевірок вище в коді. Після if ($user === null) { return; } $user уже User.
Рівень
Число від 0 до 10 у phpstan.neon або --level. Рівень N вмикає всі перевірки рівнів 0..N. level: max дорівнює найвищому.
Generics у PHPDoc
Типи з параметром, яких PHP сам не має: list<Job>, array<string, int>, Collection<int, User>. Свої класи оголошують через @template T.
Array shape
Форма масиву в PHPDoc: array{from: int, to: int, currency: string}. PHPStan перевіряє ключі й типи значень, тож одруківка в ключі стає помилкою.
Baseline
Файл phpstan-baseline.neon зі списком наявних помилок: повідомлення, ідентифікатор, файл і скільки разів. Створюється командою --generate-baseline і підключається в phpstan.neon.
@phpstan-ignore
Коментар над рядком або в ньому: // @phpstan-ignore property.nonObject. Глушить лише помилку з цим ідентифікатором і сам стає помилкою, якщо такої там немає.
Larastan
Розширення PHPStan для Laravel. Читає міграції, щоб знати колонки моделей, розуміє фасади, query builder і відношення, позначені generic-типом.
08 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з тестування →

Рівень задає набір перевірок, і кожен включає всі попередні. На рівні 8 працюють і невідомі методи з рівня 2, і типи аргументів з рівня 5, і nullable з рівня 8. Рівні 9 і 10 додають суворі правила для mixed.

Файл з наявними помилками, який PHPStan ігнорує: повідомлення, ідентифікатор, файл і кількість. Він дозволяє ввімкнути вищий рівень у старому коді й одразу ловити нові помилки. Небезпека в тому, що туди починають дописувати все нове. Тому baseline генерують лише разом з підняттям рівня, а виправлену помилку з нього прибирають: PHPStan сам повідомляє, що шаблон більше не збігається.

Перший глушить одну конкретну помилку на наступному рядку, і якщо її там немає, PHPStan про це повідомить. Другий глушить усе в рядку, включно з помилками, яких ще немає. Причину пишуть у дужках після ідентифікатора, PHPStan її пропускає.

Larastan читає міграції з database/migrations, будує з них схему таблиць і дає моделі властивості з типами колонок. Шлях до міграцій можна змінити параметром databaseMigrationsPath. Для відношень потрібен generic у PHPDoc, наприклад @return HasMany<Post, $this>, інакше Larastan не знає, що повертає $user->posts.