Увійти Реєстрація
Блог Серії
Кар'єра
Вакансії Компанії
Навчання
Документація Співбесіди Тестування Відео
Екосистема
Пакети Ресурси Проєкти Інструменти Події
Інше
Про нас Реклама
PHPStan 2.3: стрибок продуктивності, покращені generics і виявлення невикористаних змінних
Новини 06 жовтня 2026

PHPStan 2.3: стрибок продуктивності, покращені generics і виявлення невикористаних змінних

Минуло понад чотири місяці та 16 патч-релізів з часу попереднього мінорного релізу PHPStan. Тож настав час наступного: вийшов PHPStan 2.3.0. Головні теми релізу: різке прискорення аналізу, розумніший кеш результатів, покращена робота з generics та нові перевірки невикористаних змінних.

Покращення продуктивності

З початку року команда системно працювала над швидкодією PHPStan, і результат дуже помітний. За вимірами автора (5-річний MacBook Pro з M1 Pro), у грудні 2025 року аналіз коду WordPress займав 60 секунд, а сьогодні - лише 8:

Версія Дата релізу Turbo вимкнено (с) Turbo увімкнено (с) Примітка
2.1.33 5 грудня 2025 60.51 - Останній реліз до цьогорічних оптимізацій
2.1.34 19 січня 2026 46.51 - Кешування reflection-об’єктів і PHPDoc
2.1.38 30 січня 2026 44.55 - Дрібні покращення продуктивності
2.1.49 16 квітня 2026 34.49 - Подальші оптимізації
2.2.6 26 липня 2026 32.02 24.68 Перший реліз із Turbo
2.2.10 30 серпня 2026 29.94 22.20 Forking замість spawning воркерів
2.2.13 3 вересня 2026 23.06 14.12 Завжди з OPcache, прибрано runtime-перевірки
2.3.0 1 жовтня 2026 19.47 7.93 Значно більше класів «затінено» через Turbo

Начебто все просто: PHPStan прибирав роботу, яку не потрібно виконувати взагалі, яку не потрібно повторювати або яку можна зробити швидше. А в PHPStan 2.2.6 наприкінці липня з’явилося нове - PHPStan Turbo, необов’язкове розширення, що пришвидшує окремі частини PHPStan завдяки їх перереалізації на C++. Воно також вмикає інші техніки оптимізації: forking дочірніх процесів замість їх spawning (зазвичай це неможливо при запуску з PHAR-файлу) та OPcache optimizer pass, який прибирає runtime-перевірки типів у викликах PHPStan самого до себе. Про Turbo автор обіцяє окрему статтю найближчими днями.

Незважаючи на те, що розширення необов’язкове, більшість користувачів уже мають його ввімкненим автоматично й не мусять керувати ним вручну. Файли .so/.dll постачаються всередині Composer-пакета phpstan/phpstan, PHPStan сам обирає потрібний для поточного PHP-середовища та перезапускається з увімкненим розширенням через параметри командного рядка php -d.

У першому релізі Turbo PHPStan став на 23% швидшим порівняно з базовою лінією, а тепер Turbo пришвидшує його майже на 60%. Підсумок: сьогоднішній PHPStan до 7,5× швидший, ніж десять місяців тому.

Покращення кешу результатів

Це лише про «холодні» запуски, коли кешу результатів немає або він застарів. PHPStan не завжди аналізує всі файли проєкту: з березня 2020 року він запам’ятовує, які помилки були в яких файлах, перевіряє, що змінилося, і повторно аналізує лише змінені файли та ті, що від них залежать.

У повсякденній роботі користувач рідко чекає повного запуску - усе залежить від кількості змінених файлів. Ось запуски з «теплим» кешем на Drupal (великий проєкт: повний запуск - 22 секунди проти 8 для WordPress):

Файлів Частка Час (с) Примітка
0 0% 1.58 Нічого не змінилося з минулого запуску
10 0.1% 2.84
1 124 10% 6.21
2 771 25% 9.89
5 574 50% 16.10
7 204 64% 19.03
8 956 80% 21.14
11 194 100% 22.08 Кеш очищено

Повторний запуск без змін друкує результат за 1,6 секунди; якщо потрібно переаналізувати 10 файлів - 2,8 секунди; якщо 10% проєкту - 6,2 секунди.

Останні покращення гарантують, що весь кеш результатів скидається лише при оновленні PHPStan або зміні його конфігурації в проєкті. Раніше проєкт повністю переаналізовувався при оновленні будь-якої Composer-залежності або будь-якій зміні Symfony DI-контейнера. Тепер PHPStan відстежує залежності між проаналізованими файлами та встановленими пакетами, тож переаналізується лише необхідний мінімум.

Додатково з’явилися нові інтерфейси та API для розширень: кастомні розширення можуть описувати, які файли і коли треба інвалідувати, замість скидання всього кешу через будь-яку дрібницю.

Кеш результатів тепер коректно працює і в monorepo-сетапах, що використовують репозиторії "type": "path" у composer.json (документація Composer), та з scanDirectories і scanFiles для файлів, що часто змінюються.

Кеш результатів варто зберігати й відновлювати в CI для набагато швидших пайплайнів - обов’язково налаштуйте це.

Generics: більше ніякого узагальнення скалярів і багатопрохідний вивід для new Foo()

PHPStan підтримує generics майже сім років. Під час розробки доводилося робити складні вибори, а іноді - просто вгадувати, «як воно має працювати». Частина цих рішень виявилася хибною.

Наприклад, функція, що повертає той самий тип, який приймає:

/**
 * @template T
 * @param T $a
 * @return T
 */
function doFoo($a)
{
    // ...
}

Який тип слід вивести для doFoo(1): int чи 1? Єдиної правильної відповіді немає - кожен варіант має свої компроміси. PHPStan обрав узагальнення скалярів, тобто 1 ставало int. Ретроспективно це було помилкою, і користувачі роками повідомляли про багато проблем, бо хотіли зберегти точний скалярний тип. Але виправити це, не зламавши один важливий сценарій, було неможливо.

Який тип у $c = new Collection([1, 2, 3])? Якщо Collection<1|2|3>, то не вийде викликати $c->add(4). Якщо Collection<int>, то не вийде передати її в параметр із типом Collection<positive-int>, хоча це має бути валідно. А для порожньої new Collection([]) тип @template T стає never (порожній bottom type) - строго кажучи, додати в таку колекцію нічого не можна, що формально правильно, але марно в реальному світі.

Рішення запропонував Арно Ле Блан (Arnaud Le Blanc), який допоміг із понад 50 pull request для початкової реалізації generics у 2019 році: 8 березня 2022 року він описав його в issue «Bidirectional type narrowing» - звідси й назва ідеї. Автору знадобилося понад чотири роки, щоб зібрати сміливість і фінансування для коректної та ефективної реалізації.

Суть рішення: код навколо new Collection(...) аналізується кілька разів, PHPStan спостерігає, як об’єкт використовується і куди передається, і на цій основі обмежує остаточний виведений тип @template T. Приклади з інтерактивного віджета статті:

  • Передача у функцію. Якщо $ints = new Collection([]) передається в takeInts(Collection<int>), PHPStan виводить Collection<int>, а виклик takeStrings($ints) коректно дає помилку: Collection<int> замість Collection<string>.
  • Точні скаляри. Для $ids = new Collection([1, 2, 3]), який згодом передається в saveIds(Collection<positive-int>), виводиться Collection<int<1, max>>, а $ids->add(-1) повідомляє помилку, що очікується int<1, max>.
  • Виклики методів. Для $items = new Collection() із викликами add(1) та add('one') виводиться Collection<int> (якщо колекція далі передається в takeInts), а add('one') - помилка.
  • Спільний вивід. Для swap(Box<T> $a, Box<T> $b) з new Box(1) та new Box('one') обидві змінні матимуть тип Box<1|'one'>.

Перший наївний прототип сповільнював PHPStan приблизно на 30%. Після переосмислення стало зрозуміло, що переаналізовувати все не потрібно - лише рядки, на які безпосередньо впливають generic-об’єкти з нерозв’язаними template-типами. Вплив цієї можливості на продуктивність - від 1 до 3% залежно від інтенсивності використання generics, що з запасом перекривається оптимізаціями в цьому ж релізі.

Завдяки цій роботі PHPStan більше не потребує узагальнення скалярних типів у контексті generics. Реалізація закрила 17 пов’язаних issues і незліченні дублікати. Щоб скористатися покращеннями, увімкніть bleeding edge.

Двонапрямне звуження типів та пов’язану роботу замовив і зробив можливими Sovereign Tech Fund.

Виявлення невикористаних змінних

Невикористані змінні - клас помилок, який досі випадав із поля зору PHPStan. Вони можуть бути серйозними: присвоєне, але не використане значення часто означає друкарську помилку або забуту передачу значення кудись - наприклад, обчислений TTL, який так і не передали в Cache::save().

PHPStan розрізняє п’ять сценаріїв:

  • змінну присвоєно, але ніколи не прочитано - variable.unused;
  • присвоєне значення не читається до кінця тіла функції (змінну могли читати раніше, вище за присвоєння) - assign.unused;
  • присвоєне значення перезаписується іншим присвоєнням до того, як його прочитали - assign.overwritten;
  • змінній присвоюється значення, яке вона вже має - assign.redundant;
  • змінну присвоєно й нібито використано, але значення нікуди не потрапляє - assign.unusedFlow. Ідея навіяна графом потоку даних у Psalm.

Список не вичерпний. Окрім звичайних присвоєнь на кшталт $foo = ..., PHPStan перевіряє ключі масивів, які ніколи не читаються або одразу перезаписуються, результати ++ і --, ключі та значення foreach, змінні в catch, змінні use у замиканнях, а також параметри функцій і приватних методів.

Приклад variable.unused:

function sendWelcomeEmail(Mailer $mailer, User $user): void
{
    $subject = 'Welcome to Acme!';
    $mailer->send($user->getEmail(), 'Welcome!', 'Glad to have you.');
}

PHPStan повідомить: Variable $subject is never read.

Приклад assign.unusedFlow - лічильник, що збільшується, але ніде не використовується:

function importRows(Importer $importer, array $rows): void
{
    $imported = 0;
    foreach ($rows as $row) {
        $importer->import($row);
        $imported = $imported + 1;
    }
}

Тут обидва присвоєння $imported (рядки 3 і 6) отримають повідомлення, що значення «only flows into values that are never used».

У джерелі наведено інтерактивний віджет з окремими ідентифікаторами помилок для різних конструкцій:

  • Масиви: array.unusedOffset, array.offsetOverwritten, array.unusedOffsetFlow - ключі масиву, які не читаються, перезаписуються до читання або потрапляють лише в невикористані значення.
  • foreach: foreach.unusedValue, foreach.valueOverwritten, foreach.unusedValueFlow, foreach.unusedKey, foreach.keyOverwritten, foreach.unusedKeyFlow.
  • Інкременти й декременти: preInc.*, postInc.*, preDec.*, postDec.* з варіантами unused, overwritten та unusedFlow.
  • catch: catch.unusedVariableFlow - змінна винятку лише потрапляє у значення, які ніколи не використовуються.
  • Замикання: closure.unusedUse та closure.unusedUseFlow - невикористані змінні в use.
  • Функції та методи: function.unusedParameter, function.unusedParameterFlow, method.unusedParameter, method.unusedParameterFlow - невикористані параметри функцій і приватних методів.

Наприклад, для приватного методу InvoiceMailer::body(Order $order, string $email), де $email не використовується, PHPStan повідомить Method InvoiceMailer::body() has an unused parameter $email. (method.unusedParameter), а для замикання function (string $name) use ($logger, $greeting), де $logger не потрібен, - Anonymous function has an unused use $logger. (closure.unusedUse).

Повний список змін і всі деталі релізу - на сторінці PHPStan 2.3.0 на GitHub, а оригінальна стаття доступна в блозі PHPStan.

10

Читати в документації

Коментарі

Увійдіть, щоб залишити коментар

Будьте першим, хто залишить коментар!

Читайте також

Pgvector Laravel
Новини 06 жовтня 2026

Простий векторний пошук подібності для Laravel: драйвер Pgvector для Scout

Бен Бьюрстром створив драйвер Pgvector для Laravel Scout, який автоматично підтримує актуальність векторних ембедингів і дає змогу шукати дані за змістом, а не лише за ключовими словами.

11
Reservable-моделі в Laravel: як уникнути дублювання обробки за допомогою атомарних блокувань
Новини 05 жовтня 2026

Reservable-моделі в Laravel: як уникнути дублювання обробки за допомогою атомарних блокувань

Аарон Френсіс показує, як за допомогою трейта Reservable та атомарних блокувань кешу Laravel резервувати Eloquent-моделі, уникати повторної обробки та не «штурмувати» зовнішні сервіси при збоях.

9

Вакансії за темою

SQRD.tech Нова
Сьогодні

Lead/Senior Full-Stack Engineer (PHP/WordPress/JS)

Lead/Senior Full-Stack Engineer для розробки складних систем генерації лідів у сфері iGaming. Основний стек: PHP 8+ (Symfony), WordPress Multisite, JavaScript. Вимоги: 5+ років досвіду з сучасним PHP, глибокі знання архітектури WordPress, розробка складних плагінів, оптимізація продуктивності (Core Web Vitals), сильні навички CSS. Роль передбачає архітектурні рішення, власність над системами та підвищення стандартів команди, а не виконання задач.

GGLUXOUTLET Нова
3 дні тому

Senior+/Tech Lead Full-Stack WordPress Developer / Code Architect

Досвідчений Senior+/Tech Lead Full-Stack WordPress розробник для проєктування та розробки високонавантажених e-commerce рішень. Потрібні глибокі знання WordPress Core, WooCommerce, PHP 8.1+ з SOLID/ООП, React/Next.js для фронтенду, Python для автоматизації, DevOps (VPS, Docker, CI/CD) та AI-інтеграцій. Роль включає архітектуру, код-рев'ю, менторство і комунікацію з бізнесом. Мінімум 7 років досвіду, з них 3+ на WordPress/WooCommerce у високонавантажених проєктах.

Stfalcon
29 днів тому

Middle PHP (Symfony) Developer

Middle PHP-розробник на Symfony для розробки backend-частини вебзастосунків та API. Потрібен досвід 3+ років з Symfony, Doctrine ORM, PostgreSQL, RESTful API, Unit-тестування (PHPUnit) та SOLID-принципів. Основний стек: PHP 8+, Symfony, Docker, Git. Команда працює над складними системами в логістиці, транспорті, фінтеху.

Пакети за темою

Larastan

larastan/larastan

Статичний аналіз для Laravel: вчить PHPStan розуміти фасади, магічні методи Eloquent і контейнер. Ловить помилки типів і неіснуючі методи до того, як код дійде до тестів.

6,482 v3.10.0 13 1

Laravel Octane

laravel/octane

Прискорює застосунок, тримаючи його в пам'яті між запитами через FrankenPHP, Swoole або RoadRunner. Прибирає завантаження фреймворку з кожного запиту, даючи кратний приріст пропускної здатності.

4,035 v2.19.1 13 6