Оновлюємо проєкт до PHP 8.0

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

У цій статті не буде опису нових фіч, та порівняльних тестів, для початку потрібно оновити проєкт на рівні сумісності. Сьогодні ми спробуємо сформулювати план оновлення, та розглянемо основні труднощі на прикладі оновлення великого проєкту з PHP 7.4 до 8.0. Більшість етапів також будуть корисні при плануванні оновлення з більш ранніх версій.

Чому зараз?

Реліз PHP 8 відбувся 4 місяці тому. На момент написання статті вийшло три патч версії PHP 8 з виправленням важливих помилок. Також більша частина популярних бібліотек та composer пакунків нарешті додали підтримку PHP 8. Не зважаючи на те що для великої кількості бібліотек підтримка PHP 8 стосується зміни версії в composer, офіційно підтверджена сумісність усіх залежностей з vendor є блокером оновлення.

Передумови

Дана стаття є ретроспективою процесу оновлення PHP з 7.4 до 8.0 в монолітному репозиторії Oro Inc. з 11 веб застосунками та 45 модулями доволі великого проєкту, на базі Symfony 4.4 LTS, на 3M+ LoC та 600K+ PHP класів, враховуючи тести та не враховуючи vendor. Цифри не показують складність проєкту, але дають зрозуміти об’єми PHP коду, який оновлювався. Також слід зазначити, що більша частина коду не використовує strict_types=1, що теоретично ускладнює оновлення.

Основні зміни

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

Сумісність ламається лише шляхом видалення фіч, які були відмічені застарілими в минулих мінорних версіях PHP 7.0 — 7.4. Також, для покращень, розробники мови програмування можуть змінювати поведінку функцій, яка попередньо не була задокументована або з самого початку позиціонувалась як невизначена. Ось тут і зарита собака ціни оновлення.

Більша частина змін відбулася в полі строгих порівнянь та строгих типів аргументів. Ні, strict_types=1 не став поведінкою по замовчуванню. Але починаючи з восьмої версії суттєва частина вбудованих функцій PHP отримали строгу типізацію аргументів, іноді без приведення типів.

Оновлення розширень PHP

Якщо використовуються сторонні розширення PHP, розпочнемо оновлення з них. І, однак, для більшості проєктів це не є проблемою, цей пункт може заблокувати подальше оновлення.

Визначаємо пакунки Composer, які потребують оновлень

Спойлер, це був найскладніший етап оновлення для наших проєктів.

PHP багатий на готові рішення, тому зараз доволі складно знайти програму без сторонніх пакунків. В більшості випадків для управління залежностями використовується composer. Цей інструмент дає можливість визначити не тільки залежність на пакунки, а й також вказати, так звані, platform залежності кожного пакунка, до яких входить саме версія PHP та список сторонніх розширень.

Перше що приходить на думку — вказати нову версію PHP в корінному composer.json. Але не варто поспішати, оскільки це призведе до не найприємнішого ланцюга з виправлення помилок під час наступного запуску composer update. Ми вчинимо хитріше, та спершу напряму запитаємо у composer, чи є бібліотеки, які заважають оновленню.

Заходимо в корінь проєкту та виконуємо команду:

composer why-not php 8

Якщо використовуються популярні пакети та вчасно оновлюються залежності до останніх версій, через хвилину з’явиться приємне повідомлення:

Більшість же побачить «повний» список залежностей, які потрібно оновити для підтримки PHP 8. Повний тут знаходиться в лапках, тому що завжди знайдеться декілька бібліотек, які з самого початку ставили дуже слабкі обмеження platform або не вказували їх взагалі, та насправді не сумісні з PHP 8. Якщо проєкт має гарне покриття тестами, надалі воно відіграє важливу роль та допоможе виявити «пакунки — порушники». Якщо ж ні, то будемо оптимістами.

Далі, маючи на руках повний список залежностей, які не підтримують PHP 8 в поточних версіях, йдемо на packagist.org та перевіряємо, чи є версії пакунків з підтримкою нової версії мови програмування. Застосунок з OroCommerce та OroCRM має біля 300 composer пакунків в залежностях, і на сьогодні всі вони отримали сумісні оновлення. При чому в деяких випадках, як з friendsofsymfony/rest-bundle, навіть не потрібно оновлювати пакунок до останньої версії з великою кількістю незворотньо-сумісних змін.

Власний внесок в open source

Та якщо все ж знайшлись декілька пакунків, що пасуть задніх, не потрібно засмучуватись. PHP спільнота доволі активна. В більшості випадків оновлення до PHP 8 не потребує великих вкладень. Спершу перевіряємо master, можливо підтримку вже додали, але реліз ще не відбувся. Далі перевіряємо список issues та pull requests на наявність згадок PHP 8. В більшості випадків на цьому етапі вже будуть відомі причини затримки оновлень та workarounds. Але якщо бібліотека не отримала широкого використання, та попередні методи не дали результатів, завжди можна зробити свій внесок в open source. В нашому випадку було достатньо виправити тести в symfony/acl-bundle та написати коментар в issue nelmio/security-bundle, що дало змогу зменшити список на два пункти. Розробники цих бібліотек дуже активні та випустили нову версію буквально за лічені години після мінімального внеску. На жаль це не завжди трапляється так швидко, і в деяких випадках ми вимушені були чекати декілька тижнів до нового релізу, однак для більшості проєктів це все ж не є проблемою. Якщо ж бібліотека більше не підтримується, має сенс задуматись про форк або альтернативи. Можливо якісний форк вже створили до вас.

Оновлення пакунків Composer

Це напевно найдовший етап для більшості проєктів, тому що з підтримкою нової версії PHP, активні розробники розширень найчастіше випускають мажорну версію та ламають зворотну сумісність. Але гарна новина в тому що більшість змін доволі детально описані в реліз нотах або changelog в корені репозиторію: збираємо списки та маємо повний обсяг оновлення. Естімейти тут дуже залежать від того коли востаннє оновлювали залежності. Для ORO, з останнім великим оновленням понад рік тому цей етап зайняв менше ніж 100 годин.

Оцінка часу на оновлення

В залежності від того як використовується залежність, тільки інтерфейси чи велика кількість перевизначень, ціна оновлення одного пакунку може відрізнятись. Найчастіше її можна визначити перечитавши список змін та виконавши швидкий пошук по коду неймспейсів, класів та імен функцій, які змінились. Це дає можливість в короткий термін отримати приблизну оцінку часу на оновлення.

Оновлення залежностей

Дуже важливо, що здебільшого нова версія залежності з підтримкою PHP 8 все ще підтримує PHP 7.4, тому оновлення можна робити поступово, пакунок за пакунком.

Якщо ви вдало запустили composer update та дійшли до цього етапу — прийміть мої вітання, адже папка vendor, що являє собою більшу частину PHP коду в проєкті, умовно сумісна. Можемо йти далі та оновлювати безпосередньо наш код.

Хаки для несумісних залежностей

Якщо оновлюватись потрібно прямо зараз, варто протестувати сумісність несумісної бібліотеки до її оновлення на PHP 8. Для цього є кілька трюків:

1. Говоримо в composer.json що використовуємо PHP 7.4, а самі запускаємо код на PHP 8:

"platform": {"php": "7.4"}

Внаслідок того, що майже всі сторонні бібліотеки працюють одночасно і з PHP 8 і з 7.4, вдається отримати оновлення, але водночас можна вдало закінчити composer update маючи несумісні на рівні platform вимог залежності. Composer ігнорує реальну версію PHP, якщо вказана platform на рівні проєкту.

2. Перевизначити несумісні класи. Якщо використовується DiC, тоді це зазвичай доволі просто. Декоруємо, або навіть повністю підміняємо, оригінальний клас через конфігурацію контейнера. Але навіть якщо клас у vendor оголошений як final, його завжди можна перекрити на рівні autoload.classmap в composer, наприклад так. Звичайно, це брудно, але як казав Марко Півета, хак має виглядати як хак.

Оновлення власного коду

Ні, поки що не прийшов час для нових фіч. Спочатку виправимо те що зламалось. В нагоді будуть автоматичні тести. Якщо вони є, то за декілька годин можна встановити проєкт на новій версії, запустити всі тести, та оцінити обсяг подальших робіт. Наприклад, в Оро більша частина функціоналу покрита Behat, Functional та Unit тестами. Щоб зрозуміти об’єми, загальний час проходження тестів — понад 30 годин. Це дозволяє побачити повну та точну картину відразу після оновлення vendor та виправлення проблем з інсталяцією додатків.

Конфігурація перед тестуванням

Якщо розробники були сумлінними, та вже використовували строгу типізацію, то оновлення пройде доволі просто. Але і тут можна отримати підводні камені. Тому тестуємо проєкт, автоматично, чи вручну, та формулюємо список того що зламалось, виписуючи з логів exception, якщо вони є. Тестувати має сенс з увімкненими строгими повідомленнями про помилки в php.ini:

error_reporting = E_ALL 

Звісно, це за умови, якщо раніше проєкт працював в такому режимі без помилок. Інакше, в комбінації з display_errors=off, це допоможе побачити журнал помилок та попереджень не ламаючи структуру сторінок. Якщо журнал вийшов не дуже довгим, список буде корисним.

Автоматизація пошуку помилок

Завдяки AST від Нікіти Попова, в PHP існує доволі багато статичних аналізаторів, які можуть показати проблемні місця, а деколи навіть оновити код самостійно.

  • Phpstan та Psalm найпопулярніші інструменти, що допомагають підтримувати строгу типізацію. Та якщо їх не використовували в проєкті раніше, не потрібно поспішати, «помилок» вони знайдуть доволі багато, та не всі з них потребують негайного виправлення для оновлення до PHP 8.
  • Rector допоможе «переписати» код з використанням цукрових покращень синтаксису, але він не покаже нагальних проблем.
  • З подкасту «Пятиминутка PHP» я дізнався про доволі цікавий PHPCompatibility плагін для PHP_CodeSniffer, який повинен був показати проблеми. На жаль, він дав лише декілька помилкових спрацювань на нашому проєкті, тому я не можу його рекомендувати, оскільки плагін перевіряє не реальну сумісність, а згідно з документацією, яка іноді не відповідає дійсності. Наприклад, він показав заборону на використання string в неймспейсах, починаючи з версії 7.0, хоча це все ще працює.

  • Найбільш корисним виявився PhpStorm. По замовчуванню Intellij IDEA валідує лише відкриті файли, але в програмі є дуже корисна функція по аналізу всього проєкту. Якщо не писати ідеальний в розумінні шторму код, то при повному скані, помилок він знайде багато. Тут в нагоді стає можливість запустити одну конкретну інспекцію. Побачили помилку, копіюємо її назву, виділяємо папку нашого коду в дереві файлів, в меню шторму вибираємо Code > Run inspection by name, вводимо назву інспекції та дуже швидко отримуємо результат її роботи на всьому проєкті. Наприклад, при оновленні сторонніх бібліотек, інспекція «Class hierarchy checks» покаже сигнатури методів, які потрібно виправити в коді після оновлення vendor.


На що звернути увагу?

Якщо на проєкті немає автоматичних тестів, або ж покриття неповне, потрібно приділити більше часу функціям сортування та порівняння.

Сортування

Змінилась поведінка функцій сортування (usort, ...). По перше, функція порівняння елементів списку, яка використовується при сортуванні, тепер повинна повертати −1, 0 або 1.

True та false все ще працюють, але починають видавати deprecation notice. Тут в нагоді стане оператор космічний корабель <=>. Але найголовніше те, що сортування елементів з однаковою вагою тепер залишає порядок елементів без змін, що відрізняється від поведінки на PHP 7.4. Якщо проєкт достатньо великий та має модульну структуру як в Oro, то проблеми можуть з’явитись у розширень з використанням ланцюга зобов’язань (chain of responsibilities), декораторів або простого registry з сортованим списком доповнень. В нашому випадку зламалися деякі розширення, які залежали на порядок виконання, хоча пріоритет сортування не був вказаний чітко. Наприклад, змінився порядок колонок та рядків у деяких таблицях, а в інших місцях ми взагалі отримали exception.

 
usort($exportFiles, function (File $a, File $b) {
-    return $b->getMtime() > $a->getMtime();
+    return $b->getMtime() <=> $a->getMtime();
});

На малюнку нижче, лише два останні Extension мають явно зазначений пріоритет. Зверніть увагу на елемент 4:

Щоб повернути старий порядок, можна додати хак для збереження сортування, як у PHP 7:

 
$comparisonClosure = function (ExtensionVisitorInterface $a, ExtensionVisitorInterface $b) {
    if ($a->getPriority() === $b->getPriority()) {
-        return 0;
+        return -1;
    }
    return $a->getPriority() > $b->getPriority() ? -1 : 1;
} 

Нестрогі порівняння

Нестрогі порівняння чисел з нечисловими рядками тепер умовно перетворюють число в рядок та порівнюють два рядки. У нас були проблеми з першими двома прикладами:

ПорівнянняРанішеТепер
0 == "foo"
truefalse
0 == ""
truefalse
42 == "42foo"
truefalse

Строгі порівняння

Ці проблеми складно знайти в коді, але все ж про них краще знати наперед.

Як вже згадувалось вище, деякі вбудовані PHP функції отримали більш строгу перевірку типів аргументів, цей самий тренд підтримали й сторонні бібліотеки. Якщо у коді строга типізація не використовується, у більшості випадків все буде працювати як і раніше з приведенням типів. Але є і виключення, деякі PHP функції все ж викидають Error, або як мінімум deprecation warning.

Наприклад, round не приймає аргументом строку, яка не відповідає int|float, наприклад рядок «40.5» буде працювати, а «40,5» ні, або навпаки, залежно від локалі:

 
- $amount = round($row[$attributeName], 2);
+ $amount = round((float)$row[$attributeName], 2);

str_replace з declare(strict_types=1) більше не приймає другим аргументом integer оскільки очікує array|string:

 
- $alias = str_replace(WebsiteIdPlaceholder::NAME, $websiteId, $alias);
+ $alias = str_replace(WebsiteIdPlaceholder::NAME, (string)$websiteId, $alias);

Але є і дуже неявні випадки, коли поведінка змінюється мовчки. В нашому випадку php-amqplib перестав з’єднуватись з rabbit mq завдяки тому, що в функцію ми передавали пустий масив, коли очікувався null. В минулій версії при нестрогому порівнянні це працювало зовсім інакше.

 
- $this->channel->wait([], false, $timeout);
+ $this->channel->wait(null, false, $timeout);

Іменовані аргументи

Проблеми від цієї доволі суперечливої фічі ми отримали ще до початку її використання.

Наприклад, тепер не можна передавати асоціативний масив аргументом до array_merge:

 
- $data = call_user_func_array('array_merge', $indexData);
+ $data = array_merge(...array_values($indexData));

Більшість проблем з іменованими аргументами ми отримали з call_user_func_array.

Error замість return false

Ось декілька прикладів змін з нашого коду:

get_parent_class:

 
- $parentClassName = get_parent_class($className);
+ $parentClassName = class_exists($className) ? get_parent_class($className) : false;

method_exists:

 
- } elseif (method_exists($domainObject, 'getId')) {
+ } elseif (null !== $domainObject && method_exists($domainObject, 'getId')) {

Або все ж ловимо помилку:

 
- return new \NumberFormatter($locale, $style))->format($value);
+ try {
+     return new \NumberFormatter($locale, $style))->format($value);
+ } catch (\TypeError $error) {
+     return $value;
+ }

Це лише обов’язкові зміни, які були необхідні при оновленні одного репозиторію на 3`000`000 рядків PHP коду до версії 8.0, які мало описані в документації. Повніший список можна знайти в офіційному путівнику по оновленню.

Результати

Так скільки ж рядків коду в результаті довелось оновити в репозиторіях Oro для підтримки PHP 8? :)

Як бачимо, оновлення пройшло доволі просто та загалом допомогло знайти не найякісніший код. Оскільки метою була підтримка PHP 8.0, без необхідності використання нових фіч, а розробники МП намагались зберегти зворотну сумісність, PR вийшов доволі малим, 86 файлів з +316, −257 змінених рядків. Це без урахування правок в composer.json та composer.lock. В першу чергу PR такий малий внаслідок того що ми завершили оновлення більшої частини vendor три місяці тому, в рамках підготовки LTS релізу. Процес оновлення, без урахування ревью, на всьому монолітному репозиторії зайняв близько 40 годин, що доволі мало за мірками попередніх оновлень технологій в Oro. Звісно, оцінка буде відрізнятись залежно від кількості та змісту коду, але зважаючи на те, що більшість проєктів в PHP здебільшого високорівневі, набагато більше проблем, ніж у нас, вийти не повинно.

Що далі?

А далі можна використовувати всі нові фічі для покращення читабельності крутості коду. Як мінімум у новому коді. Як максимум, якщо не страшно втратити git blame, в усьому проєкті, завдяки автоматизації, яку дає Rector, та схожі інструменти.

Вмикаємо JIT, схрещуємо пальці, та запускаємо ApacheBench.

Ну і чекаємо на PHP 8.1, у якому нарешті з’являються enums, на революцію від fibers, та що всі ці перевірки типів, які в PHP працюють в runtime на відміну від мов зі строгою типізацією, нарешті будуть менше уповільнювати додатки ;).

Висновок

Отже, на основі практичного досвіду оновлення до PHP 8 великої кодової бази ми бачимо, що:

  • спільнота розробників open source бібліотек активно слідкує за релізами PHP і доволі оперативно додає підтримку нових версій PHP у своїх бібліотеках;
  • дотримання авторами бібліотек суворої дисципліни з перерахуванням лише фактично підтримуваних версій PHP у composer.json значно допомагає розробникам, які використовують ці бібліотеки у своїх проєктах завдяки вбудованому у composer аналізатору залежностей;
  • PHPStorm є не тільки чудовою IDE для написання коду, а ще й надає інструменти статичного аналізу коду, що допомагають у процесі міграції кодової бази на нові версії PHP;
  • високе покриття автоматизованими тестами значно спрощує процес міграції;
  • а оновлення до нових мажорних версій PHP не таке складне, як може здатися на перший погляд.

Цікавого вам оновлення!

👍НравитсяПонравилось9
В избранноеВ избранном1
LinkedIn
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

А не краще на новій версії PHP розробляти нову версію продукту, замість того щоб натягувати стару?

Ти не вказав головного — а чи є взагалі вигода від перенесення старих проектів на 8.0? Якщо із переходом на php7 вигода була у суттєвому зростанні швидкодії, то про 8 нічого такого не чув.

Ця тема вже добре розжована за 4 місяці з моменту реліза. Тому я лише коротко натякнув на результати оновлення. Вигода в першу чергу для розробників, перше посилання в розділі «Що далі?» дає відповіді на твое питання.

Загалом JIT не дає суттєвого зростання швидкості, причини добре описав Стогов: www.youtube.com/watch?v=7UOWus-5yxg.

Ми очікуєму більшого від PHP 8.1 за рахунок Inheritance Cache.

Подписаться на комментарии