Полное руководство по транспиляции PHP-кода
Опубликовано: 2021-09-22В идеальных обстоятельствах мы должны использовать PHP 8.0 (последняя версия на момент написания этой статьи) для всех наших сайтов и обновлять ее, как только будет выпущена новая версия. Однако разработчикам часто приходится работать с предыдущими версиями PHP, например, при создании общедоступного плагина для WordPress или при работе с устаревшим кодом, который препятствует обновлению среды веб-сервера.
В таких ситуациях мы можем отказаться от использования самого последнего PHP-кода. Но есть лучшая альтернатива: мы по-прежнему можем писать исходный код на PHP 8.0 и транспилировать его в предыдущую версию PHP — даже в PHP 7.1.
В этом руководстве мы научим вас всему, что вам нужно знать о транспиляции PHP-кода.
Что такое транспиляция?
Транспиляция преобразует исходный код из языка программирования в эквивалентный исходный код того же или другого языка программирования.
Транспиляция — не новая концепция в веб-разработке: клиентские разработчики, скорее всего, знакомы с Babel, транспилером для кода JavaScript.
Babel преобразует код JavaScript из современной версии ECMAScript 2015+ в устаревшую версию, совместимую со старыми браузерами. Например, для стрелочной функции ES2015:
[2, 4, 6].map((n) => n * 2);…Babel преобразует его в свою версию ES5:
[2, 4, 6].map(function(n) { return n * 2; });Что такое транспиляция PHP?
Потенциально новым в веб-разработке является возможность транспиляции серверного кода, в частности PHP.
Транспиляция PHP работает так же, как транспиляция JavaScript: исходный код из современной версии PHP преобразуется в эквивалентный код для более старой версии PHP.
Следуя тому же примеру, что и раньше, функция стрелки из PHP 7.4:
$nums = array_map(fn($n) => $n * 2, [2, 4, 6]);…можно преобразовать в эквивалентную версию PHP 7.3:
$nums = array_map( function ($n) { return $n * 2; }, [2, 4, 6] );Стрелочные функции можно транспилировать, поскольку они представляют собой синтаксический сахар, то есть новый синтаксис для создания существующего поведения. Это низко висящие плоды.
Однако есть также новые функции, которые создают новое поведение, и поэтому не будет эквивалентного кода для предыдущих версий PHP. Это относится к типам объединения, представленным в PHP 8.0:
function someFunction(float|int $param): string|float|int|null { // ... }В этих ситуациях транспиляция все еще может быть выполнена, если новая функция требуется для разработки, а не для производства. Затем мы можем просто полностью удалить функцию из транспилируемого кода без серьезных последствий.
Одним из таких примеров являются типы объединения. Эта функция используется для проверки отсутствия несоответствия между типом ввода и предоставленным значением, что помогает предотвратить ошибки. Если есть конфликт с типами, то ошибка будет уже в разработке, и мы должны ее поймать и исправить до того, как код попадет в продакшен.
Следовательно, мы можем позволить себе удалить функцию из кода для производства:
function someFunction($param) { // ... }Если ошибка все еще происходит в рабочей среде, выданное сообщение об ошибке будет менее точным, чем если бы у нас были типы объединения. Однако этот потенциальный недостаток перевешивается возможностью использования типов объединения.
Преимущества транспиляции PHP-кода
Транспиляция позволяет закодировать приложение, используя последнюю версию PHP, и создать выпуск, который также работает в средах, использующих более старые версии PHP.
Это может быть особенно полезно для разработчиков, создающих продукты для устаревших систем управления контентом (CMS). WordPress, например, по-прежнему официально поддерживает PHP 5.6 (хотя и рекомендует PHP 7.4+). Процент сайтов WordPress, использующих версии PHP с 5.6 по 7.2, которые все находятся в состоянии End-of-Life (EOL), что означает, что они больше не получают обновления безопасности, составляет 34,8%, а те, которые работают на любой версии PHP, отличной от 8.0 составляет колоссальные 99,5%:

Следовательно, темы и плагины WordPress, ориентированные на глобальную аудиторию, скорее всего, будут написаны с использованием старой версии PHP, чтобы увеличить их возможный охват. Благодаря транспиляции их можно было закодировать с использованием PHP 8.0 и по-прежнему выпускать для более старой версии PHP, таким образом ориентируя максимальное количество пользователей.
Действительно, любое приложение, которое должно поддерживать любую версию PHP, кроме самой последней (даже в пределах диапазона поддерживаемых в настоящее время версий PHP), может выиграть.
Это относится к Drupal, для которого требуется PHP 7.3. Благодаря транспиляции разработчики могут создавать общедоступные модули Drupal с использованием PHP 8.0 и выпускать их с PHP 7.3.
Другой пример — создание пользовательского кода для клиентов, которые по той или иной причине не могут запустить PHP 8.0 в своей среде. Тем не менее, благодаря транспиляции разработчики по-прежнему могут кодировать свои продукты, используя PHP 8.0, и запускать их в этих устаревших средах.
Когда транспилировать PHP
Код PHP всегда можно транспилировать, если только он не содержит какой-либо функции PHP, не имеющей эквивалента в предыдущей версии PHP.
Возможно, это относится к атрибутам, представленным в PHP 8.0:
#[SomeAttr] function someFunc() {} #[AnotherAttr] class SomeClass {}В более раннем примере со стрелочными функциями код можно было транспилировать, потому что стрелочные функции — это синтаксический сахар. Атрибуты, напротив, создают совершенно новое поведение. Это поведение также может быть воспроизведено с PHP 7.4 и ниже, но только путем ручного кодирования, т. е. не автоматически на основе инструмента или процесса (искусственный интеллект может предоставить решение, но мы еще не достигли этого).
Атрибуты, предназначенные для использования в разработке, такие как #[Deprecated] , могут быть удалены так же, как удаляются типы объединения. Но атрибуты, которые изменяют поведение приложения в рабочей среде, нельзя ни удалить, ни напрямую передать.
На сегодняшний день ни один транспилятор не может взять код с атрибутами PHP 8.0 и автоматически создать эквивалентный ему код PHP 7.4. Следовательно, если ваш PHP-код должен использовать атрибуты, его транспиляция будет затруднена или невозможна.
Возможности PHP, которые можно транспилировать
Это функции из PHP 7.1 и выше, которые в настоящее время могут быть перенесены. Если ваш код использует только эти функции, вы можете быть уверены, что транспилированное приложение будет работать. В противном случае вам нужно будет оценить, приведет ли транспилированный код к сбоям.
| PHP-версия | Функции |
|---|---|
| 7.1 | Все |
| 7.2 | - тип object- расширение типа параметра - Флаг PREG_UNMATCHED_AS_NULL в preg_match |
| 7.3 | — Присвоение ссылок в list() / деструктуризация массива ( кроме внутри foreach — #4376)– Гибкий синтаксис Heredoc и Nowdoc – Завершающие запятые в вызовах функций - set(raw)cookie принимает аргумент $option |
| 7.4 | – Типизированные свойства – стрелочные функции – Нулевой объединяющий оператор присваивания – Распаковка внутри массивов – Числовой литеральный разделитель - strip_tags() с массивом имен тегов– ковариантные возвращаемые типы и контравариантные типы параметров |
| 8,0 | – Типы союзов – mixed псевдотип- static тип возврата– Магическая константа ::class на объектах- match выражения— catch исключения только по типу– Null-безопасный оператор - Продвижение свойства конструктора класса – Завершающие запятые в списках параметров и списках use замыканий. |
Транспиляторы PHP
В настоящее время существует один инструмент для транспиляции PHP-кода: Rector.
Rector — это инструмент реконструктора PHP, который преобразует код PHP на основе программируемых правил. Мы вводим исходный код и набор правил для запуска, а Rector преобразует код.
Rector управляется через командную строку, устанавливается в проект через Composer. При выполнении Rector выведет «diff» (добавления — зеленым, удаления — красным) кода до и после конвертации:

В какую версию PHP транспилировать
Для переноса кода между версиями PHP необходимо создать соответствующие правила.
Сегодня библиотека Rector включает в себя большинство правил транспиляции кода в диапазоне от PHP 8.0 до 7.1. Следовательно, мы можем надежно транспилировать наш PHP-код вплоть до версии 7.1.
Существуют также правила переноса с PHP 7.1 на 7.0 и с 7.0 на 5.6, но они не являются исчерпывающими. Ведутся работы по их завершению, так что в конечном итоге мы можем транспилировать PHP-код до версии 5.6.
Транспиляция против бэкпорта
Backporting похож на транспиляцию, но проще. Код резервного копирования не обязательно зависит от новых функций языка. Вместо этого ту же функциональность можно предоставить более старой версии языка, просто скопировав/вставив/адаптировав соответствующий код из новой версии языка.
Например, функция str_contains появилась в PHP 8.0. Эту же функцию для PHP 7.4 и ниже можно легко реализовать следующим образом:
if (!defined('PHP_VERSION_ID') || (defined('PHP_VERSION_ID') && PHP_VERSION_ID < 80000)) { if (!function_exists('str_contains')) { /** * Checks if a string contains another * * @param string $haystack The string to search in * @param string $needle The string to search * @return boolean Returns TRUE if the needle was found in haystack, FALSE otherwise. */ function str_contains(string $haystack, string $needle): bool { return strpos($haystack, $needle) !== false; } } }Поскольку резервное копирование проще, чем транспиляция, мы должны выбирать это решение всякий раз, когда обратное портирование выполняет свою работу.
Что касается диапазона между PHP 8.0 и 7.1, мы можем использовать полифилл-библиотеки Symfony:
- Полифил PHP 7.1
- Полифил PHP 7.2
- Полифил PHP 7.3
- Полифил PHP 7.4
- Полифил PHP 8.0
Эти библиотеки поддерживают следующие функции, классы, константы и интерфейсы:
| PHP-версия | Функции |
|---|---|
| 7.2 | Функции:
Константы:
|
| 7.3 | Функции:
Исключения:
|
| 7.4 | Функции:
|
| 8,0 | Интерфейсы:
Классы:
Константы:
Функции:
|
Примеры транспилированного PHP
Давайте рассмотрим несколько примеров транспилируемого PHP-кода и несколько полностью транспилируемых пакетов.
PHP-код
Выражение match было введено в PHP 8.0. Этот исходный код:
function getFieldValue(string $fieldName): ?string { return match($fieldName) { 'foo' => 'foofoo', 'bar' => 'barbar', 'baz' => 'bazbaz', default => null, }; } … будет транспилирован в эквивалентную версию PHP 7.4 с помощью оператора switch :
function getFieldValue(string $fieldName): ?string { switch ($fieldName) { case 'foo': return 'foofoo'; case 'bar': return 'barbar'; case 'baz': return 'bazbaz'; default: return null; } }Оператор nullsafe также появился в PHP 8.0:
public function getValue(TypeResolverInterface $typeResolver): ?string { return $this->getResolver($typeResolver)?->getValue(); }Передаваемый код должен сначала присвоить значение операции новой переменной, чтобы избежать повторного выполнения операции:
public function getValue(TypeResolverInterface $typeResolver): ?string { return ($val = $this->getResolver($typeResolver)) ? $val->getValue() : null; }Функция повышения свойств конструктора, также представленная в PHP 8.0, позволяет разработчикам писать меньше кода:
class QueryResolver { function __construct(protected QueryFormatter $queryFormatter) { } }При транспиляции для PHP 7.4 создается полный фрагмент кода:
class QueryResolver { protected QueryFormatter $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }Транспилированный код выше содержит типизированные свойства, которые были введены в PHP 7.4. При переносе этого кода в PHP 7.3 они заменяются блоками документов:
class QueryResolver { /** * @var QueryFormatter */ protected $queryFormatter; function __construct(QueryFormatter $queryFormatter) { $this->queryFormatter = $queryFormatter; } }PHP-пакеты
Следующие библиотеки транспилируются для производства:
| Библиотека/описание | Код/примечания |
|---|---|
| Ректор Инструмент реконструктора PHP, который делает возможной транспиляцию | - Исходный код - Транспилированный код - Примечания |
| Стандарты простого кодирования Инструмент для того, чтобы код PHP соответствовал набору правил | - Исходный код - Транспилированный код - Примечания |
| GraphQL API для WordPress Плагин, предоставляющий сервер GraphQL для WordPress | - Исходный код - Транспилированный код - Примечания |
Плюсы и минусы транспиляции PHP
Преимущество транспиляции PHP уже было описано: это позволяет исходному коду использовать PHP 8.0 (т. е. последнюю версию PHP), которая будет преобразована в более низкую версию PHP для производства для запуска в устаревшем приложении или среде.
Это эффективно позволяет нам стать лучшими разработчиками, производя код более высокого качества. Это связано с тем, что наш исходный код может использовать типы объединения PHP 8.0, типизированные свойства PHP 7.4, а также различные типы и псевдотипы, добавляемые в каждую новую версию PHP ( mixed из PHP 8.0, object из PHP 7.2), среди другие современные возможности PHP.
Используя эти функции, мы можем лучше выявлять ошибки во время разработки и писать код, который легче читать.
Теперь давайте рассмотрим недостатки.
Он должен быть закодирован и поддерживаться
Rector может транспилировать код автоматически, но этот процесс, скорее всего, потребует некоторого ручного ввода, чтобы он работал с нашей конкретной настройкой.
Сторонние библиотеки также должны быть транспилированы
Это становится проблемой всякий раз, когда их транспиляция приводит к ошибкам, поскольку затем мы должны углубиться в их исходный код, чтобы выяснить возможную причину. Если проблема может быть устранена, а проект имеет открытый исходный код, нам нужно будет отправить запрос на включение. Если библиотека не с открытым исходным кодом, мы можем столкнуться с препятствием.
Ректор не сообщает нам, когда код не может быть транспилирован
Если исходный код содержит атрибуты PHP 8.0 или любую другую функцию, которую нельзя транспилировать, мы не можем продолжить. Однако Ректор не проверит это условие, поэтому нам нужно сделать это вручную. Это может не быть большой проблемой в отношении нашего собственного исходного кода, поскольку мы уже знакомы с ним, но это может стать препятствием в отношении сторонних зависимостей.
Отладочная информация использует транспилированный код, а не исходный код
Когда приложение выдает сообщение об ошибке с трассировкой стека в рабочей среде, номер строки будет указывать на транспилированный код. Нам нужно обратно преобразовать транспилированный код в исходный, чтобы найти соответствующий номер строки в исходном коде.
Транспилируемый код также должен иметь префикс
Наш транспилированный проект и некоторые другие библиотеки, также установленные в производственной среде, могут использовать ту же стороннюю зависимость. Эта сторонняя зависимость будет перенесена для нашего проекта и сохранит исходный код для другой библиотеки. Следовательно, транспилированная версия должна иметь префикс с помощью PHP-Scoper, Strauss или какого-либо другого инструмента, чтобы избежать потенциальных конфликтов.
Транспиляция должна выполняться во время непрерывной интеграции (CI)
Поскольку транспилированный код естественным образом перекрывает исходный код, нам не следует запускать процесс транспиляции на наших компьютерах для разработки, иначе мы рискуем создать побочные эффекты. Запуск процесса во время запуска CI более удобен (подробнее об этом ниже).
Как транспилировать PHP
Для начала нам нужно установить Rector в наш проект для разработки:
composer require rector/rector --dev Затем мы создаем файл конфигурации rector.php в корневом каталоге проекта, содержащий необходимые наборы правил. Чтобы понизить код с PHP 8.0 до 7.1, мы используем эту конфигурацию:

use Rector\Set\ValueObject\DowngradeSetList; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $containerConfigurator->import(DowngradeSetList::PHP_80); $containerConfigurator->import(DowngradeSetList::PHP_74); $containerConfigurator->import(DowngradeSetList::PHP_73); $containerConfigurator->import(DowngradeSetList::PHP_72); }; Чтобы убедиться, что процесс выполняется должным образом, мы можем запустить команду process Rector в сухом режиме, передав расположение (я) для обработки (в данном случае все файлы в папке src/ ):
vendor/bin/rector process src --dry-run Чтобы выполнить транспиляцию, мы запускаем команду process Rector, которая изменит файлы в их существующем местоположении:
vendor/bin/rector process src Обратите внимание: если мы запустим rector process на наших компьютерах для разработки, исходный код будет преобразован на месте в src/ . Однако мы хотим создать преобразованный код в другом месте, чтобы не переопределять исходный код при понижении версии кода. По этой причине запуск процесса наиболее удобен во время непрерывной интеграции.
Оптимизация процесса транспиляции
Чтобы сгенерировать транспилированный результат для производства, необходимо преобразовать только код для производства; код, необходимый только для разработки, можно пропустить. Это означает, что мы можем избежать переноса всех тестов (как для нашего проекта, так и для его зависимостей) и всех зависимостей для разработки.
Что касается тестов, мы уже знаем, где находятся тесты для нашего проекта — например, в папкеtests tests/ . Мы также должны выяснить, где находятся зависимости для зависимостей — например, в их подпапках test tests/ , test/ и Test/ (для разных библиотек). Затем мы говорим Rector пропустить обработку этих папок:
return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ // Skip tests '*/tests/*', '*/test/*', '*/Test/*', ]); }; Что касается зависимостей, Composer знает, какие из них предназначены для разработки (под записью require-dev в composer.json ), а какие для производства (под записью require ).
Чтобы получить из Composer пути всех зависимостей для производства, мы запускаем:
composer info --path --no-devЭта команда создаст список зависимостей с их именем и путем, например:
brain/cortex /Users/leo/GitHub/leoloso/PoP/vendor/brain/cortex composer/installers /Users/leo/GitHub/leoloso/PoP/vendor/composer/installers composer/semver /Users/leo/GitHub/leoloso/PoP/vendor/composer/semver guzzlehttp/guzzle /Users/leo/GitHub/leoloso/PoP/vendor/guzzlehttp/guzzle league/pipeline /Users/leo/GitHub/leoloso/PoP/vendor/league/pipeline Мы можем извлечь все пути и передать их команде Rector, которая затем обработает папку src/ нашего проекта, а также папки, содержащие все зависимости для производства:
$ paths="$(composer info --path --no-dev | cut -d' ' -f2- | sed 's/ //g' | tr '\n' ' ')" $ vendor/bin/rector process src $pathsДальнейшее улучшение может помешать Rector обрабатывать эти зависимости, уже использующие целевую версию PHP. Если библиотека была закодирована с помощью PHP 7.1 (или любой версии ниже), то нет необходимости транспилировать ее в PHP 7.1.
Для этого мы можем получить список библиотек, требующих PHP 7.2 и выше, и обработать только их. Мы получим имена всех этих библиотек с помощью команды « why-not » Composer, например:
composer why-not php "7.1.*" | grep -o "\S*\/\S*" Поскольку эта команда не работает с флагом --no-dev , чтобы включить только зависимости для производства, нам сначала нужно удалить зависимости для разработки и перегенерировать автозагрузчик, выполнить команду, а затем добавить их снова:
$ composer install --no-dev $ packages=$(composer why-not php "7.1.*" | grep -o "\S*\/\S*") $ composer install Команда Composer info --path извлекает путь к пакету в следующем формате:
# Executing this command $ composer info psr/cache --path # Produces this response: psr/cache /Users/leo/GitHub/leoloso/PoP/vendor/psr/cacheМы выполняем эту команду для всех элементов в нашем списке, чтобы получить все пути для переноса:
for package in $packages do path=$(composer info $package --path | cut -d' ' -f2-) paths="$paths $path" done Наконец, мы предоставляем этот список Ректору (плюс папку src/ проекта):
Вам нужно решение для хостинга, которое даст вам конкурентное преимущество? Kinsta обеспечит вас невероятной скоростью, ультрасовременной безопасностью и автоматическим масштабированием. Ознакомьтесь с нашими планами
vendor/bin/rector process src $pathsПодводные камни, которых следует избегать при транспиляции кода
Транспиляция кода может считаться искусством, часто требующим специальных настроек для проекта. Давайте рассмотрим несколько проблем, с которыми мы можем столкнуться.
Связанные правила не всегда обрабатываются
Сцепленное правило — это когда правилу необходимо преобразовать код, созданный предыдущим правилом.
Например, библиотека symfony/cache содержит такой код:
final class CacheItem implements ItemInterface { public function tag($tags): ItemInterface { // ... return $this; } } При переносе с PHP 7.4 на 7.3 tag функции должен претерпеть две модификации:
- Тип возвращаемого
ItemInterfaceдолжен быть сначала преобразован вselfиз-за правилаDowngradeCovariantReturnTypeRector. - Затем необходимо удалить возвращаемый тип
selfиз-за правилаDowngradeSelfTypeDeclarationRector.
Конечный результат должен быть таким:
final class CacheItem implements ItemInterface { public function tag($tags) { // ... return $this; } }Однако Rector выводит только промежуточный этап:
final class CacheItem implements ItemInterface { public function tag($tags): self { // ... return $this; } }Проблема в том, что ректор не всегда может контролировать порядок применения правил.
Решение состоит в том, чтобы определить, какие связанные правила остались необработанными, и выполнить новый запуск Rector, чтобы применить их.
Чтобы определить связанные правила, мы дважды запускаем Rector в исходном коде, например:
$ vendor/bin/rector process src $ vendor/bin/rector process src --dry-run В первый раз мы запускаем Rector, как и ожидалось, для выполнения транспиляции. Во второй раз мы используем флаг --dry-run , чтобы узнать, нужно ли еще внести изменения. Если они есть, команда завершится с кодом ошибки, а в выводе «diff» будет указано, какие правила еще можно применить. Это означало бы, что первый запуск не был завершен, и какое-то цепочечное правило не обрабатывалось.

Как только мы определили неприменимое связанное правило (или правила), мы можем создать еще один файл конфигурации Rector — например, rector-chained-rule.php выполнит отсутствующее правило. Вместо обработки полного набора правил для всех файлов в src/ на этот раз мы можем запустить конкретное отсутствующее правило для конкретного файла, где его необходимо применить:
// rector-chained-rule.php use Rector\Core\Configuration\Option; use Rector\DowngradePhp74\Rector\ClassMethod\DowngradeSelfTypeDeclarationRector; use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; return static function (ContainerConfigurator $containerConfigurator): void { $services = $containerConfigurator->services(); $services->set(DowngradeSelfTypeDeclarationRector::class); $parameters = $containerConfigurator->parameters(); $parameters->set(Option::PATHS, [ __DIR__ . '/vendor/symfony/cache/CacheItem.php', ]); }; Наконец, мы сообщаем Rector при его втором проходе использовать новый файл конфигурации через ввод --config :
# First pass with all modifications $ vendor/bin/rector process src # Second pass to fix a specific problem $ vendor/bin/rector process --config=rector-chained-rule.phpЗависимости Composer могут быть несовместимыми
Библиотеки могут объявить зависимость для разработки (т.е. в require-dev в composer.json ), но при этом ссылаться на некоторый код из них для производства (например, на некоторые файлы в src/ , а не в tests/ ).
Обычно это не проблема, потому что этот код может не загружаться в рабочей среде, поэтому в приложении никогда не будет ошибки. Однако, когда Rector обрабатывает исходный код и его зависимости, он проверяет возможность загрузки всего кода, на который ссылаются. Rector выдаст ошибку, если какой-либо файл ссылается на какой-то фрагмент кода из неустановленной библиотеки (поскольку он был заявлен как необходимый только для разработки).
Например, класс EarlyExpirationHandler из компонента Symfony Cache реализует интерфейс MessageHandlerInterface из компонента Messenger:
class EarlyExpirationHandler implements MessageHandlerInterface { //... } Однако symfony/cache объявляет symfony/messenger зависимостью для разработки. Затем при запуске Rector в проекте, который зависит от symfony/cache , он выдаст ошибку:
[ERROR] Could not process "vendor/symfony/cache/Messenger/EarlyExpirationHandler.php" file, due to: "Analyze error: "Class Symfony\Component\Messenger\Handler\MessageHandlerInterface not found.". Include your files in "$parameters->set(Option::AUTOLOAD_PATHS, [...]);" in "rector.php" config. See https://github.com/rectorphp/rector#configuration".Есть три решения этой проблемы:
- В конфигурации Rector пропустите обработку файла, который ссылается на этот фрагмент кода:
return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::SKIP, [ __DIR__ . '/vendor/symfony/cache/Messenger/EarlyExpirationHandler.php', ]); };- Загрузите отсутствующую библиотеку и добавьте ее путь для автоматической загрузки Rector:
return static function (ContainerConfigurator $containerConfigurator): void { // ... $parameters->set(Option::AUTOLOAD_PATHS, [ __DIR__ . '/vendor/symfony/messenger', ]); };- Ваш проект зависит от отсутствующей библиотеки для производства:
composer require symfony/messengerТранспиляция и непрерывная интеграция
Как упоминалось ранее, на наших компьютерах для разработки мы должны использовать флаг --dry-run при запуске Rector, иначе исходный код будет переопределен транспилированным кодом. По этой причине более удобно запускать фактический процесс транспиляции во время непрерывной интеграции (CI), когда мы можем запускать временные исполнители для выполнения процесса.
Идеальное время для выполнения процесса транспиляции — создание релиза для нашего проекта. Например, приведенный ниже код представляет собой рабочий процесс для GitHub Actions, который создает выпуск плагина WordPress:
name: Generate Installable Plugin and Upload as Release Asset on: release: types: [published] jobs: build: name: Build, Downgrade and Upload Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/[email protected] - name: Downgrade code for production (to PHP 7.1) run: | composer install vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.php - name: Build project for production run: | composer install --no-dev --optimize-autoloader mkdir build - name: Create artifact uses: montudor/[email protected] with: args: zip -X -r build/graphql-api.zip . -x *.git* node_modules/\* .* "*/\.*" CODE_OF_CONDUCT.md CONTRIBUTING.md ISSUE_TEMPLATE.md PULL_REQUEST_TEMPLATE.md rector.php *.dist composer.* dev-helpers** build** - name: Upload artifact uses: actions/[email protected] with: name: graphql-api path: build/graphql-api.zip - name: Upload to release uses: JasonEtco/[email protected] with: args: build/graphql-api.zip application/zip env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Этот рабочий процесс содержит стандартную процедуру выпуска плагина WordPress через GitHub Actions. Новое дополнение для переноса кода плагина из PHP 7.4 в 7.1 происходит на этом шаге:
- name: Downgrade code for production (to PHP 7.1) run: | vendor/bin/rector process sed -i 's/Requires PHP: 7.4/Requires PHP: 7.1/' graphql-api.phpВ совокупности этот рабочий процесс теперь выполняет следующие шаги:
- Проверяет исходный код плагина WordPress из своего репозитория, написанного на PHP 7.4.
- Устанавливает свои зависимости Composer
- Транспилирует свой код из PHP 7.4 в 7.1
- Изменяет запись «Требуется PHP» в заголовке основного файла плагина с
"7.4"на"7.1". - Удаляет зависимости, необходимые для разработки
- Создает .zip-файл плагина, исключая все ненужные файлы.
- Загружает ZIP-файл в качестве ресурса выпуска (и, кроме того, в качестве артефакта в действие GitHub).
Тестирование транспилированного кода
После того, как код был транспилирован в PHP 7.1, откуда мы знаем, что он работает хорошо? Или, другими словами, как мы узнаем, что он был полностью преобразован и не осталось никаких остатков более высоких версий PHP-кода?
Подобно транспиляции кода, мы можем реализовать решение в процессе CI. Идея состоит в том, чтобы настроить среду запуска с PHP 7.1 и запустить линтер на транспилированном коде. Если какой-либо фрагмент кода несовместим с PHP 7.1 (например, типизированное свойство из PHP 7.4, которое не было преобразовано), линтер выдаст ошибку.
Хорошо работающим линтером для PHP является PHP Parallel Lint. Мы можем установить эту библиотеку как зависимость для разработки в нашем проекте или сделать так, чтобы процесс CI установил ее как отдельный проект Composer:
composer create-project php-parallel-lint/php-parallel-lintВсякий раз, когда код содержит PHP 7.2 и выше, PHP Parallel Lint выдает ошибку, подобную этой:
Run php-parallel-lint/parallel-lint layers/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php PHP 7.1.33 | 10 parallel jobs ............................................................ 60/2870 (2 %) ............................................................ 120/2870 (4 %) ... ............................................................ 660/2870 (22 %) .............X.............................................. 720/2870 (25 %) ............................................................ 780/2870 (27 %) ... ............................................................ 2820/2870 (98 %) .................................................. 2870/2870 (100 %) Checked 2870 files in 15.4 seconds Syntax error found in 1 file ------------------------------------------------------------ Parse error: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php:55 53| '0.8.0', 54| \__('GraphQL API for WordPress', 'graphql-api'), > 55| ))) { 56| $plugin->setup(); 57| } Unexpected ')' in layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/graphql-api.php on line 55 Error: Process completed with exit code 1.Давайте добавим линтер в рабочий процесс нашего CI. Шаги, которые необходимо выполнить для переноса кода из PHP 8.0 в 7.1 и его тестирования:
- Проверьте исходный код
- Запустите среду PHP 8.0, чтобы Ректор мог интерпретировать исходный код.
- Перенесите код в PHP 7.1.
- Установите инструмент линтера PHP
- Переключите версию PHP среды на 7.1.
- Запустите линтер на транспилированном коде
Этот рабочий процесс GitHub Action выполняет следующие задачи:
name: Downgrade PHP tests jobs: main: name: Downgrade code to PHP 7.1 via Rector, and execute tests runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/[email protected] - name: Set-up PHP uses: shivammathur/[email protected] with: php-version: 8.0 coverage: none - name: Local packages - Downgrade PHP code via Rector run: | composer install vendor/bin/rector process # Prepare for testing on PHP 7.1 - name: Install PHP Parallel Lint run: composer create-project php-parallel-lint/php-parallel-lint --ansi - name: Switch to PHP 7.1 uses: shivammathur/[email protected] with: php-version: 7.1 coverage: none # Lint the transpiled code - name: Run PHP Parallel Lint on PHP 7.1 run: php-parallel-lint/parallel-lint src/ vendor/ --exclude vendor/symfony/polyfill-ctype/bootstrap80.php --exclude vendor/symfony/polyfill-intl-grapheme/bootstrap80.php --exclude vendor/symfony/polyfill-intl-idn/bootstrap80.php --exclude vendor/symfony/polyfill-intl-normalizer/bootstrap80.php --exclude vendor/symfony/polyfill-mbstring/bootstrap80.php Обратите внимание, что несколько файлов bootstrap80.php из полифилловых библиотек Symfony (которые не нужно транспилировать) должны быть исключены из линтера. Эти файлы содержат PHP 8.0, поэтому при их обработке линтер будет выдавать ошибки. Однако исключение этих файлов безопасно, так как они будут загружены в рабочей среде только при работе с PHP 8.0 или более поздней версии:
if (\PHP_VERSION_ID >= 80000) { return require __DIR__.'/bootstrap80.php'; }
Резюме
В этой статье мы узнали, как транспилировать наш PHP-код, что позволило нам использовать PHP 8.0 в исходном коде и создать выпуск, работающий на PHP 7.1. Транспиляция выполняется с помощью Rector, инструмента реконструкции PHP.
Транспиляция нашего кода делает нас лучшими разработчиками, поскольку мы можем лучше выявлять ошибки в разработке и создавать код, который, естественно, легче читать и понимать.
Транспиляция также позволяет нам отделить наш код от конкретных требований PHP от CMS. Теперь мы можем сделать это, если хотим использовать последнюю версию PHP для создания общедоступного плагина WordPress или модуля Drupal, не сильно ограничивая нашу пользовательскую базу.
Остались вопросы по транспиляции PHP? Дайте нам знать в разделе комментариев!
