Эта статья не о новых функциях или сравнительных тестах; Во-первых, нам нужно обновить проект, чтобы он был совместим с PHP 8.

Сегодня мы составим план обновления и обсудим основные потенциальные трудности на примере обновления большого проекта с PHP 7.4 до 8.0. Большинство шагов также будут полезны при планировании обновления с более ранних версий.

Почему сейчас?

PHP 8 был выпущен четыре месяца назад. На момент написания есть версии PHP 8 с тремя патчами, которые исправляют критические ошибки. Кроме того, в наиболее популярные библиотеки и композиторские пакеты наконец-то добавлена ​​поддержка PHP 8. Хотя для многих библиотек поддержка PHP 8 предполагает изменение версии в композиторе, официально подтвержденная совместимость всех зависимостей папки vendor является препятствием для обновления.

Предпосылки

Эта статья представляет собой ретроспективу процесса обновления PHP с 7.4 до 8.0 в монолитном репозитории Oro Inc. с 11 веб-приложениями и 45 модулями довольно большого проекта на основе Symfony 4.4 LTS, 3M + LoC и более 600 000 классов PHP. , включая тесты, но исключая поставщика.

Цифры не отражают сложность проекта, но указывают на количество обновленного кода PHP. Следует также отметить, что большая часть кода не использует strict_types = 1, что технически усложняет обновление.

Значительные перемены

Помимо новых функций, которым в большинстве случаев есть альтернатива, основной выпуск позволяет разработчикам языка PHP нарушить обратную совместимость. Но это не так уж и плохо, поскольку нет необходимости переписывать большую часть проекта для обновления.

Совместимость нарушается только при удалении функций, которые были помечены как устаревшие в предыдущих дополнительных версиях PHP 7.0–7.4. Для внесения улучшений разработчики ядра PHP также могут изменить поведение функций, которые ранее не были задокументированы или с самого начала позиционировались как неопределенные.

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

Обновление расширений PHP

Если вы используете сторонние расширения PHP, сначала обновите их. Хотя для большинства проектов это не должно быть большой проблемой, это может заблокировать дальнейшее обновление.

Определение пакетов Composer, требующих обновления

Предупреждение о спойлере: обновление зависимостей было самым сложным этапом обновления.

PHP имеет множество готовых решений, поэтому сложно найти приложение без сторонних зависимостей. В большинстве случаев для управления ими используется композитор. Этот инструмент позволяет определять зависимости от пакетов и указывать так называемые зависимости от платформы для каждого пакета, который включает версию PHP и список необходимых расширений PHP.

Первое, что приходит в голову, - это указать новую версию PHP в корневом каталоге composer.json, но не торопитесь, так как это вызовет неприятную цепочку исправлений ошибок в следующий раз, когда вы запустите обновление композитора. Лучше всего напрямую спросить композитора, могут ли какие-либо библиотеки помешать обновлению. Для этого перейдите в корень проекта и выполните следующую команду:

composer why-not php 8

Если вы используете популярные пакеты и своевременно обновляете зависимости до последних версий, вы увидите это сообщение примерно через минуту:

Большинство увидит «полный» список зависимостей, которые необходимо обновить для поддержки PHP 8. Complete заключен здесь в кавычки, потому что всегда есть несколько библиотек, которые с самого начала устанавливают очень слабые ограничения платформы или не указывают их совсем, что означает, что они фактически несовместимы с PHP 8. Если проект имеет хорошее тестовое покрытие, он будет играть важную роль в будущем и поможет идентифицировать такие пакеты. Если нет, то давайте будем оптимистами.

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

Вклад в открытый исходный код

Если вы нашли несколько пакетов, которые отстают, не о чем беспокоиться. Сообщество PHP довольно активно. В большинстве случаев обновление до PHP 8 не требует значительных вложений. Сначала проверьте главную ветку, чтобы узнать, добавлена ​​ли уже поддержка и будет ли она включена в следующий выпуск. Затем проверьте список проблем и запрос на включение упоминаний PHP 8. В большинстве случаев причины задержки обновлений и обходные пути будут определены уже на этом этапе.

Однако, если библиотека широко не используется и предыдущие методы не дали результатов, вы всегда можете внести свой вклад в открытый исходный код. В нашем случае достаточно было исправить тесты в symfony / acl-bundle и написать комментарий к проблеме в nelmio / security-bundle. Это позволило сократить список на два пункта. Будучи очень активными, разработчикам этих библиотек удалось выпустить новую версию всего за несколько часов после минимального вклада. Не всегда это происходит так быстро. В некоторых случаях приходилось ждать выхода новой версии несколько недель. Для большинства проектов это не большая проблема. Если библиотека больше не поддерживается, есть смысл подумать о форках или альтернативах; возможно, для вас уже создан качественный форк.

Обновление пакетов Composer

Обновление пакетов композитора, вероятно, является самым долгим этапом для большинства проектов, потому что, наряду с поддержкой новой версии PHP, активные разработчики расширений часто выпускают основную версию и нарушают обратную совместимость. Но хорошая новость заключается в том, что большинство изменений подробно описано в примечаниях к выпуску или в журнале изменений в корне репозитория. Составьте списки изменений, и это большая часть вашего объема обновления.

Оценка времени обновления

В зависимости от того, как используется зависимость (например, используются ли только интерфейсы или существует множество переопределений), усилия по обновлению одного пакета могут различаться. Его часто можно определить, прочитав список изменений и выполнив быстрый поиск кода по пространствам имен, классам и именам функций, которые изменились. Это позволяет быстро получить приблизительную оценку времени обновления.

Обновление зависимостей

Важно отметить, что по большей части новая версия зависимостей с поддержкой PHP 8 по-прежнему поддерживает PHP 7.4, поэтому обновление можно проводить постепенно, пакет за пакетом.

Если вы успешно завершили обновление композитора и дошли до этого этапа, поздравляем! Папка поставщика, которая обычно содержит большую часть кода PHP в проекте, теоретически совместима. Мы можем пойти дальше и обновить код напрямую.

Хаки для несовместимых зависимостей

Если вам нужно выполнить обновление сейчас, чтобы проверить совместимость несовместимой библиотеки, прежде чем обновлять ее до PHP 8, вот несколько уловок, чтобы сделать это:

  • Сообщите композитору, что вы используете PHP 7.4, но запускаете код на PHP 8:
"platform": {"php": "7.4"}

Поскольку почти все сторонние библиотеки работают одновременно с PHP 8 и 7.4, вы можете получать обновления и, в то же время, успешно завершить обновление композитора с несовместимыми требованиями зависимостей на уровне платформы. Composer игнорирует реальную версию PHP, если указана конфигурация платформы на уровне проекта.

  • Переопределите несовместимые классы. Если используется DiC, то обычно это довольно просто. Украсить или даже полностью переопределить исходный класс с помощью конфигурации контейнера. Даже если класс в вендоре объявлен как final, его всегда можно переопределить на уровне autoload.classmap в composer.json, например, как this. Конечно, это беспорядочно, но, как сказал Марко Пиветта, взлом должен выглядеть как взлом.

Обновление вашего кода

Еще не время для новых функций, поскольку вам сначала нужно исправить то, что сломалось. Здесь вам пригодятся автоматические тесты. С их помощью вы можете установить проект на новую версию, запустить все тесты и оценить объем дальнейшей работы за несколько часов. Например, в Oro большая часть функциональности покрывается Behat, функциональными и модульными тестами. Чтобы вы имели представление о количестве тестов, общее время тестирования составляет более 30 часов. Это позволяет нам видеть полную и точную картину сразу после обновления поставщика и устранения проблем с установкой приложения.

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

Если ваши коллеги-разработчики проявили усердие и уже используют строгую типизацию, обновление должно пройти гладко; однако уже тогда вы можете столкнуться с потенциальными проблемами. Вот почему так важно протестировать проект автоматически или вручную и составить список неисправностей, выписывая исключения из журналов, если они есть. Тестирование имеет смысл, если в php.ini включены строгие сообщения об ошибках:

error_reporting = E_ALL

Конечно, это при условии, что ранее проект работал в этом режиме без ошибок. В противном случае в сочетании с display_errors = off это поможет вам увидеть журнал ошибок и предупреждений, не нарушая структуру страниц. Если журнал небольшой, этот список окажется весьма полезным.

Автоматическое обнаружение ошибок

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

  • Phpstan и Psalm - самые популярные инструменты, помогающие поддерживать строгий набор текста. Но если они еще не использовались в проекте, не нужно торопиться, так как они обнаружат много проблем. Однако не все из них потребуют немедленного исправления для обновления до PHP 8.
  • Rector поможет «переписать» код, используя улучшения синтаксического сахара, но не покажет никаких срочных проблем.
  • Из подкаста «Five Minutes PHP» я узнал о довольно интересном плагине PHPCompatibility для PHP_CodeSniffer, который должен был выявить проблемы. К сожалению, на самом деле он показал лишь несколько ложных срабатываний в нашем проекте, поэтому я не могу его рекомендовать. Плагин не проверяет реальную совместимость; вместо этого он проверяет совместимость согласно документации, которая не всегда соответствует действительности. Например, он показал, что мы не можем использовать «строку» в пространствах имен, начиная с версии 7.0, хотя она по-прежнему работает без ошибок.

PhpStorm оказался, безусловно, самым полезным. По умолчанию IntelliJ IDEA проверяет только открытые файлы, но имеет функцию анализа всего проекта. Для этого код, который вы пишете, должен быть идеальным с точки зрения PhpStorm; в противном случае после полного сканирования может быть обнаружено множество ошибок. Здесь мы можем запустить одну конкретную проверку.

Когда вы видите ошибку, скопируйте ее имя, выберите папку кода в дереве файлов, выберите Код ›Выполнить проверку по имени в меню PhpStorm, введите имя проверки. Очень быстро вы получите результат для всего проекта. Например, при обновлении сторонних библиотек «Проверка иерархии классов» покажет сигнатуры методов, которые необходимо исправить в коде после обновления поставщика.

Изменения, о которых следует помнить

Если на проекте нет автоматических тестов или покрытие неполное, нужно обратить внимание на функции сортировки и сравнения.

Сортировка

Изменилось поведение функций сортировки (usort,…). Во-первых, функция сравнения, используемая при сортировке, теперь должна возвращать -1, 0 или 1.

Истины и ложь по-прежнему работают, но вызывают уведомление об устаревании. Здесь вам поможет оператор космического корабля «‹=›«. Но самое главное, сортировка элементов с одинаковым весом теперь оставляет порядок элементов неизменным, что отличается от поведения в PHP 7.4. Предположим, проект достаточно большой и имеет модульную структуру, как у Oro. В этом случае могут возникнуть проблемы с расширениями, которые реализованы с использованием цепочки обязанностей, декораторов или простого реестра с отсортированным списком надстроек. В нашем случае некоторые расширения, зависящие от порядка выполнения, были нарушены, потому что приоритет сортировки не был четко указан. Например, порядок столбцов и строк в некоторых таблицах изменился, а в других местах мы получили исключение.

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

Хак для сохранения сортировки, как в PHP 7:

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

На рисунке ниже только два последних расширения имеют явный приоритет. Здесь обратите внимание на пункт 4:

Нестрогие сравнения

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

╔═══════════════╦════════╦═══════╗
║  Comparison   ║ Before ║  Now  ║
╠═══════════════╬════════╬═══════╣
║ 0  == "foo"   ║  true  ║ false ║
║ 0  == ""      ║  true  ║ false ║
║ 42 == "42foo" ║  true  ║ false ║
╚═══════════════╩════════╩═══════╝

Строгие сравнения

Эти проблемы сложно найти в коде, но о них лучше узнать заранее.

Как упоминалось выше, некоторые встроенные функции PHP переключились на строго типизированные аргументы, и сторонние библиотеки поддержали ту же тенденцию. Если в коде не используется строгая типизация, в большинстве случаев все будет работать с приведением типов. Но бывают исключения; некоторые функции PHP выдают ошибку или вызывают предупреждение об устаревании.

Например, функция round не принимает строковый аргумент, не соответствующий подписи int | float. Строка «40,5» будет работать, а «40,5» - нет, или наоборот, в зависимости от локали:

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

str_replace с declare (strict_types = 1) больше не принимает целое число в качестве второго аргумента, поскольку ожидает массив | строку:

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

Но есть и менее очевидные случаи, когда поведение меняется незаметно. В нашем случае php-amqplib перестал подключаться к RabbitMQ, потому что мы передали в функцию пустой массив, когда ожидалось значение null. В предыдущей версии, при вольном сравнении, все работало совершенно иначе.

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

Именованные аргументы

Мы столкнулись с проблемами из-за этой довольно противоречивой функции еще до того, как начали ее использовать.

Например, теперь вы не можете безопасно передать ассоциативный массив с аргументом в call_user_func_array:

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

Ошибка вместо возврата 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 000 строк кода PHP до версии 8.0, которые мало описаны в документации. Вы можете найти полный список в официальном руководстве по обновлению.

Полученные результаты

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

Как видите, обновление было довольно простым и в целом помогло найти код не самого лучшего качества. Поскольку целью была поддержка PHP 8.0 без использования новых функций, а разработчики пытались сохранить обратную совместимость, запрос на вытягивание оказался относительно небольшим: 86 файлов с +316 и 257 измененными строками. Это без учета изменений в composer.json и composer.lock.

Запрос на вытягивание невелик, в первую очередь потому, что мы завершили большую часть обновлений поставщика три месяца назад в рамках подготовки к выпуску LTS. Процесс обновления, без учета обзоров, занял около 40 часов во всем монолитном репозитории, что относительно мало по опыту предыдущих технологических обновлений в Oro. Конечно, оценка будет варьироваться в зависимости от объема и содержания кода, но поскольку большинство проектов PHP в основном высокоуровневые, у вас не должно возникнуть гораздо больше проблем, чем у нас.

Что дальше?

Теперь вы можете использовать все новые функции для улучшения читабельности кода - по крайней мере, в новом коде. В лучшем случае, если вы не боитесь потерять git blame, вы можете использовать их во всем проекте благодаря автоматизации, предоставляемой Rector, и аналогичным инструментам.

Включите JIT, скрестите пальцы и запустите ApacheBench.

И ждите PHP 8.1 (который, наконец, будет включать перечисления), революцию от волокон, и когда все эти проверки типов, которые работают во время выполнения в PHP, будут меньше тормозить приложение.

Заключение

Основываясь на практическом опыте обновления большой кодовой базы до PHP 8, мы видим, что:

  • Сообщество разработчиков ПО с открытым исходным кодом активно отслеживает выпуски PHP и довольно быстро добавляет поддержку новых версий PHP в свои библиотеки;
  • Соблюдая строгую дисциплину со стороны авторов библиотек, перечисление только тех версий PHP, которые фактически поддерживаются в composer.json, очень помогает разработчикам, которые используют эти библиотеки в своих проектах, благодаря встроенному в композитор анализатору зависимостей;
  • PHPStorm - это не только отличная IDE для написания кода, но также предоставляет инструменты статического анализа кода, которые помогут вам перенести кодовую базу на более новые версии PHP;
  • Большой охват автоматическими тестами значительно упрощает процесс миграции;
  • Обновление до новых основных версий PHP не так сложно, как может показаться на первый взгляд.

Наслаждайтесь обновлением!