← Назад в блог

Два плагина, одна библиотека, сломанный сайт: изоляция зависимостей на сборке

Опубликовано
3 мин чтения
--- просмотров

Каждый плагин WordPress, использующий Composer, везёт собственный каталог vendor/. На сайте с двадцатью плагинами это двадцать автозагрузчиков в одном процессе PHP.

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

Механика

Имена классов в PHP глобальны. Автозагрузчик плагина A зарегистрировался первым и подгрузил Vendor\Library\Client из своей копии. Автозагрузчик плагина B для этого класса уже не сработает: класс существует, и B молча получает версию из A.

Если A везёт вторую версию, а B рассчитывает на третью, ломается B. Выглядит это как фатальная ошибка об отсутствующем методе, в плагине B, на строке, которая была верной два года. Ни один из авторов не воспроизведёт такое у себя, потому что ни у одного не стоит соседний плагин.

Библиотеки капчи попадают в эту ловушку постоянно. Вместе с ними HTTP-клиенты, реализации PSR и всё из семейств Symfony и Illuminate, то есть ровно те пакеты, которые плагин утаскивает к себе не задумываясь.

Решения, которые не работают

«Просто затребуйте ту же версию». Не выйдет: вы не управляете чужим плагином, его графиком релизов и тем, поддерживается ли он вообще.

«Проверьте, существует ли класс». Функция class_exists() говорит, что класс загружен, а вовсе не что это нужная вам версия. Прикрыв собственное определение, вы просто становитесь тем плагином, который молча работает на чужом коде.

«Загрузим наш vendor первым». Теперь гонку выигрываете вы, а ломается сосед. Это не починка, а выбор пострадавшего.

Переименовать зависимость на сборке

Устойчивый ответ один: перестать делить имя. PHP-Scoper на сборке переписывает пространства имён всего содержимого vendor/, и ваша копия становится YourPrefix\Vendor\Library\Client. Теперь два плагина везут две версии одной библиотеки, и ни один не видит чужую.

Настройка в основном про то, что переименовывать нельзя:

$wp_classes   = json_decode(file_get_contents(__DIR__ .
    '/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-classes.json'), true);
$wp_functions = json_decode(file_get_contents(__DIR__ .
    '/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-functions.json'), true);
$wp_constants = json_decode(file_get_contents(__DIR__ .
    '/vendor/sniccowp/php-scoper-wordpress-excludes/generated/exclude-wordpress-constants.json'), true);

Собственные символы WordPress должны остаться нетронутыми. Префикс у add_action даёт плагин, который грузится и ничего не делает. Пакет sniccowp/php-scoper-wordpress-excludes ведёт эти списки за вас.

Интерфейсы и контракты обычно тоже надо исключать:

$excluded = [
    '/^Psr\\\\/',
    '/^Symfony\\\\Contracts\\\\/',
    '/^Illuminate\\\\Contracts\\\\/',
];

Интерфейс PSR работает как общий словарь. Добавите префикс к Psr\Log\LoggerInterface, и ваш логгер перестанет подходить под чужие type hints, то есть под объявленные типы аргументов. Тогда исчезнет совместимость, ради которой стандарт и придумывали. Префиксуйте реализации, контракты оставьте в покое.

Цена, честно

Скоупинг не бесплатен.

У сборки появляется шаг. Разработка идёт без скоупинга, продакшен со скоупингом, и это не побайтово одно и то же. Баг, воспроизводящийся только в собранной версии, стоит очень неприятного вечера.

Динамические имена классов ломаются. Конструкции new $className и call_user_func('Vendor\Thing::method') внутри это строки, скоупер их не видит. Их надо править или исключать.

Стектрейсы становятся уродливее: префикс появляется в каждом кадре.

Взвешивайте это против альтернативы, то есть бага, который вы не воспроизведёте, о котором сообщит пользователь и в котором обвинит ваш плагин за поломку чужого.

Общее правило

Если ваш код будет выполняться в процессе, которым вы не управляете, не рассчитывайте, что имя принадлежит вам одному. Плагины WordPress тут очевидный случай, но та же логика покрывает браузерные расширения на одной странице, библиотеки внутри хост-приложения и всё, что внедряется в среду, куда внедряется ещё кто-то.

Глобальные имена работают как общий ресурс. Упаковать зависимость значит занять часть этого пространства, и вежливое поведение здесь совпадает с устойчивым: занимайте ту часть, которая никому больше не понадобится.

Открыт для работы по контракту

Я доступен для работы по контракту. Если у вас есть интересная идея проекта, запишитесь на звонок через Calendly.

Записаться на 30-минутный звонок