Помилки, знайдені без запуску
Метод find() повертає ?User, а контролер одразу читає $user->name. Тести проходять, бо в них користувач завжди є. На продакшні хтось відкриває профіль за старим посиланням, і сторінка падає з TypeError. PHPStan бачить цей рядок, не запускаючи жодного: він виводить з сигнатур і PHPDoc і перевіряє кожен вираз.
каже, які перевірки ввімкнено, і кожен додає свої до попередніх. 0–2: невідомі класи, методи й змінні, коректність PHPDoc. 3–5: типи повернення й властивостей, мертві гілки, типи аргументів. 6: тип має бути скрізь, а масиви потребують на кшталт list<Job> або . 7–8: частково хибні union-типи й звертання до nullable без перевірки. 9–10: mixed стає суворим, спершу явний, потім і неявний. Повзунок на головній сторінці розділу показує це на одному контролері.
Старий проєкт на рівні 8 видасть сотні помилок. записує їх у файл, і далі PHPStan падає лише на нових. Окреме місце, де ви знаєте більше за аналізатор, глушать . Магію Laravel PHPStan сам не розуміє: властивості моделей, фасади, query builder пояснює йому розширення .
Одна помилка, пʼять способів з нею жити
Рівень 8 каже про ProfileController::show(): Cannot access property $name on User|null. [identifier=property.nonObject]. Перемикайте вкладки: від виправлення до того, як цю помилку відкладають.
Що буває без PHPStan
-
1
рядки 10–16
find() чесно обіцяє ?User: користувача може не бути. PHPStan вірить цьому типу більше, ніж тестам, де користувач є завжди.
-
2
рядки 22–28
Рівень 8: Cannot access property $name on User|null. Без PHPStan про це скаже лише запит з неіснуючим id.
-
3
рядки 30–36
Рівень 9: json_decode() повертає mixed, і брати з нього title не можна, доки тип не звужено. Рівень 8 цього ще не бачить.
-
4
рядки 39–57
Обвʼязка: попередження PHP друкуються у вивід, TypeError перехоплюється. PHPDoc callable(): string тримає цей код чистим навіть на рівні 10.
-
5
рядки 59–67
Два вдалі виклики і три невдалі. Кожен невдалий PHP помічає лише в момент виконання, і лише для цього вхідного значення.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
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 там безпечний.
Як PHPStan перестає допомагати
Одразу на рівень 9
Команда вмикає високий рівень у проєкті, який ніколи не аналізували, отримує дві тисячі помилок і вимикає PHPStan на тиждень, тобто назавжди.
Натомість: піднімайте по одному рівню: --level наступного, --generate-baseline, коміт. Нові помилки ловляться з першого дня, а baseline зменшується разом з рефакторингом.
Baseline як смітник
Кожну нову помилку дописують у baseline, бо «так швидше». Через рік у ньому тисяча рядків, і рівень у phpstan.neon більше нічого не означає.
Натомість: перегенеровуйте baseline лише разом з підняттям рівня. У рев'ю зростання phpstan-baseline.neon має бути так само помітним, як вимкнений тест.
Глухі ігнори
// @phpstan-ignore-line або ignoreErrors з регулярним виразом без path. Такий ігнор ховає й ту помилку, яку додадуть у цей рядок чи файл завтра.
Натомість: @phpstan-ignore з ідентифікатором і причиною в дужках. Якщо помилки з цим ідентифікатором там немає, PHPStan про це скаже, і зайвий ігнор не залишиться.
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.
Що робити з новою помилкою PHPStan
- Виведення типів
- 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-типом.
Питання зі співбесід
Рівень задає набір перевірок, і кожен включає всі попередні. На рівні 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.