🍾 Gato GraphQL теперь использует scoping, благодаря PHP-Scoper!
Плагин Gato GraphQL теперь использует scoping. Это означает, что плагин наконец-то можно загрузить в директорию плагинов WordPress.

Для этого я использую замечательный PHP-Scoper. Работа с этой библиотекой в WordPress сопряжена с определёнными трудностями, поэтому в этой статье я объясню, как мне удалось с этим справиться.
Разделы:
- Принятие решения о применении scoping
- Изучение вариантов
- Попытка использовать Mozart, и провал
- Знакомство с PHP-Scoper, и паника
- Возвращение к PHP-Scoper, теперь уже насовсем
- PHP-Scoper, простой способ 😎 👈🏽 Здесь начинается моё решение
- Покажи настоящее дело
- Тестирование
- Посмотрите на результаты
Принятие решения о применении scoping
Несколько недель назад Мэтт Малленвег объявил, что будет следить за «плагином GraphQL», очевидно имея в виду WPGraphQL. Его высказывание свидетельствует о том, что он считает, будто существует только один плагин GraphQL, тогда как на самом деле их два (тот, что остался за кадром, — ну, мой). Это заставило меня осознать, насколько мало известен мой плагин, и мне стало не по себе.
Мэтт не знал, что мой плагин существует. Впрочем, как и большая часть сообщества WordPress. Очевидно, я недостаточно его продвигаю. Я знаю, что плохо разбираюсь в маркетинге и социальных сетях; технические вещи у меня получаются получше (или мне так кажется). Поэтому я решил что-то предпринять, хотя бы в меру своих возможностей.
Итак, вот над чем я работаю:
- Я только что закончил разработку этого самого сайта, gatographql.com, и запустил его 2 недели назад (ура! 🥳 Кстати, как он вам? Буду рад отзывам — через DM или email)
- 3 дня назад я наконец-то начал применять scoping к плагину и закончил вчера! (В 3 часа ночи, но оно того стоило 😅)
- И наконец, я уже работаю над следующей версией
0.8, которая станет первой доступной в репозитории плагинов
Применение scoping к плагину обязательно для загрузки его в репозиторий, иначе он может конфликтовать с другим плагином, который требует ту же зависимость, что и мой плагин, но с другой версией. Это действительно важный рубеж; никакая другая разработка не имеет такого значения. Например, мне ещё предстоит завершить схему GraphQL, чтобы она полностью соответствовала модели данных WordPress, но это будет постепенно делаться с каждым новым релизом.
Так что через несколько недель плагин будет появляться при поиске «GraphQL», и люди, которым действительно нужно реализовать GraphQL API, узнают о существовании моего плагина.
Я действительно хочу, чтобы мой плагин всерьёз рассматривался как вариант для будущего WordPress. Я работаю над ним несколько лет. Репозиторий был создан ещё в августе 2016 года — это даже раньше, чем появился WPGraphQL, и в самом начале GraphQL. Но я не знал, что проект станет GraphQL-сервером; это направление обозначилось лишь около 1,5 лет назад.
(По сути, проект представляет собой фреймворк для создания приложений с использованием серверных компонентов, и GraphQL-сервер вполне можно построить на этой архитектуре. Вот я его и построил.)
WPGraphQL — зарелизившийся плагин, и по праву: он был запущен несколько лет назад, и вокруг него сформировалось сообщество. Работа Jason Bahl (который работает в Gatsby) и контрибьюторов его проекта была выдающейся: интеграция WordPress в Jamstack теперь проще, чем когда-либо.
Но одно дело — Gatsby и Jamstack, и другое дело — WordPress. WordPress занимает 40% веба, а не просто является источником данных для генератора статических сайтов.
Теперь мы можем задаться вопросом, подходит ли WPGraphQL, не имея этого решения, принятого за нас из-за отсутствия альтернатив. Теперь мы можем проанализировать оба плагина, чтобы понять, чьи цели лучше соответствуют тому, что важно для WordPress.
Gato GraphQL тоже умеет работать с Jamstack. Но его главные цели, я считаю, более грандиозны: «демократизировать публикацию данных», чтобы редактирование API было таким же простым, как редактирование записи (что может сделать каждый), и превратить WordPress в ОС веба.
Когда плагин появится в репозитории, я надеюсь, что больше людей попробуют его и скажут: «Эй, это же чертовски здорово! Как же я не знал об этом раньше?»
И тогда выбор «плагина GraphQL» не будет предопределён, и сообщество WordPress сможет рассматривать как WPGraphQL, так и Gato GraphQL на основе их собственных достоинств.
Теперь, когда с мотивацией покончено, поговорим о технических вещах 🤓.
Изучение вариантов
Применение scoping к плагину предполагает запуск специальных инструментов, которые берут код плагина в качестве входных данных и выдают плагин со scoping. Не так сложно, правда? Насколько это может быть трудно?

Ну, в зависимости от кодовой базы одного лишь выполнения команды scope может оказаться недостаточно. После этого нужно проверить ошибки в консоли, исправить их, тщательно протестировать приложение, определить ошибки и их причины, исправить их и повторять снова. Чтобы сделать всё правильно, может потребоваться некоторое время.
Существуют 2 библиотеки для scoping с разными целями:
- Mozart — для кода WordPress
- PHP-Scoper — для любого PHP-кода, в особенности при создании PHAR-архивов
Поскольку у меня плагин для WordPress, я сначала попробовал Mozart. Посмотрим, что из этого вышло.
Попытка использовать Mozart, и провал
Я пробовал Mozart около 1 года назад. По словам документации, «команда mozart compose делает всю магию». Поэтому я ожидал, что всё будет очень быстро и просто, и остаток дня можно будет провести за дайкири.
Увы, Mozart так и не заработал с моей кодовой базой. Он постоянно наталкивался на проблемы, поэтому scoping так и не материализовался. И я не смог получить необходимую помощь: я отправил PR, но его не рассмотрели для слияния, и меня даже не уведомили об этом, поэтому я продолжал ждать, пока естественным образом не потерял интерес к этому проекту.
Я думаю, что Mozart не мог справиться с некоторыми зависимостями в моём плагине. Я использую несколько компонентов Symfony, включая DependencyInjection, Cache и Dotenv, и всем этим управляет Composer.
Scoping PHP — это не только про PHP, поэтому у scopper'а будет много препятствий, которые нужно обойти, и задач, которые нужно решить. Например, Symfony DependencyInjection использует файлы YAML для настройки конфигурации, и их тоже нужно обработать scoping. А файл composer.json содержит конфигурацию автозагрузки PSR-4, и это тоже нужно обработать. И, я полагаю, Mozart не справлялся с этими сложностями должным образом.
Но я уверен, что мой опыт — не единственный, и что там немало довольных пользователей. Кроме того, моя неудачная попытка была 1 год назад, и я не знаю, улучшился ли инструмент с тех пор. И ещё не забудьте поговорку: «Все плагины со scoping похожи друг на друга; каждый плагин без scoping — без scoping по-своему», так что, возможно, это не работает только у меня.
Если ваш плагин WordPress прост, содержит самодостаточную логику и scoping должен выполняться только в PHP-коде, то Mozart, скорее всего, справится. Просто попробуйте.
Знакомство с PHP-Scoper, и паника
Итак, я обратился к PHP-Scoper. Однако я даже не попытался его попробовать, потому что сразу испугался.
Для начала, этот инструмент не поддерживает WordPress из коробки. А далее они рекомендуют взглянуть на их собственный Makefile, который выглядит вот так:
# See https://tech.davis-hansson.com/p/make/
MAKEFLAGS += --warn-undefined-variables
MAKEFLAGS += --no-builtin-rules
.DEFAULT_GOAL := help
PHPBIN=php
PHPNOGC=php -d zend.enable_gc=0
IS_PHP8=$(shell php -r "echo version_compare(PHP_VERSION, '8.0.0', '>=') ? 'true' : 'false';")
SRC_FILES=$(shell find bin/ src/ -type f)
.PHONY: help
help:
@echo "\033[33mUsage:\033[0m\n make TARGET\n\n\033[32m#\n# Commands\n#---------------------------------------------------------------------------\033[0m\n"
@fgrep -h "##" $(MAKEFILE_LIST) | fgrep -v fgrep | sed -e 's/\\$$//' | sed -e 's/##//' | awk 'BEGIN {FS = ":"}; {printf "\033[33m%s:\033[0m%s\n", $$1, $$2}'
#
# Build
#---------------------------------------------------------------------------
.PHONY: clean
clean: ## Clean all created artifacts
clean:
git clean --exclude=.idea/ -ffdx
update-root-version: ## Check the lastest GitHub release and update COMPOSER_ROOT_VERSION accordingly
update-root-version:
rm .composer-root-version || true
$(MAKE) .composer-root-versionИ ещё 600 строк в том же духе. Похоже на ребус. Решив, что мне нужно понять этот код только для того, чтобы применить scoping к своему плагину, я сбежал без лишних церемоний.
(Ну, понимание этого кода — их рекомендация для тестирования приложения со scoping, но это не обязательно. Мы также можем просто запустить команду php-scoper add-prefix, позволить ей сделать всю магию и пойти выпить наш дайкири.)
Возвращение к PHP-Scoper, теперь уже насовсем
Итак, 3 дня назад я принял решение реализовать scoping, любым способом. Мне нужно было это сделать.
Я вернулся к PHP-Scoper, чтобы по-настоящему его попробовать. Я знал, что WordPress можно обработать с его помощью, прочитав PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (от блестящих ребят из Delicious Brains). Это было лишь вопросом настроя и упорства.
Я изучил некоторые из существующих решений, в том числе:
Но все они меня не вполне устраивают: либо код выглядит как хак, либо хрупким и готовым сломаться в любой момент.
Например, плагин Google Web Stories применяет scoping к коду, а затем откатывает каждый из конфликтов:
return [
'patchers' => [
function ( $file_path, $prefix, $contents ) {
/*
* There is currently no easy way to simply whitelist all global WordPress functions.
*
* This list here is a manual attempt after scanning through the AMP plugin, which means
* it needs to be maintained and kept in sync with any changes to the dependency.
*
* As long as there's no built-in solution in PHP-Scoper for this, an alternative could be
* to generate a list based on php-stubs/wordpress-stubs. devowlio/wp-react-starter/ seems
* to be doing just this successfully.
*
* @see https://github.com/humbug/php-scoper/issues/303
* @see https://github.com/php-stubs/wordpress-stubs
* @see https://github.com/devowlio/wp-react-starter/
*/
$contents = str_replace( "\\$prefix\\_doing_it_wrong", '\\_doing_it_wrong', $contents );
$contents = str_replace( "\\$prefix\\__", '\\__', $contents );
$contents = str_replace( "\\$prefix\\esc_html_e", '\\esc_html_e', $contents );
$contents = str_replace( "\\$prefix\\esc_html", '\\esc_html', $contents );
$contents = str_replace( "\\$prefix\\esc_attr", '\\esc_attr', $contents );
$contents = str_replace( "\\$prefix\\esc_url", '\\esc_url', $contents );
$contents = str_replace( "\\$prefix\\do_action", '\\do_action', $contents );
// ...
}
]
]Я понимаю, почему они так делают, но мне это не нравится. Каждый раз, когда появляется новая функция WordPress, им нужно убедиться, что она тоже попала в этот список. Слишком вручную, слишком хрупко.
Таков был мой вызов: неужели нет более простого способа применить scoping к плагину, используя код, который не стыдно показать друзьям и коллегам?
PHP-Scoper, простой способ 😎
На деле всё оказалось проще, чем я думал! Всего за несколько часов всё заработало.

Когда я говорю «просто» и «часы», я имею в виду следующее: всё заработало сразу, но только после 2 месяцев создания правильной структуры кодовой базы (объясню подробнее позже).
Но главное вот в чём: если у вас есть правильная настройка проекта, применить к нему scoping можно очень быстро.
Проблема применения scoping к коду WordPress — это, ну, код WordPress. Проблема объясняется здесь, но суть в том, что все функции и классы WordPress тоже помещаются в namespace. Таким образом, если мы ссылаемся на WP_Query или вызываем get_posts в нашем коде, они будут преобразованы в MyPrefixedNamespace\WP_Query и MyPrefixedNamespace\get_posts, что приведёт к катастрофическому сбою во время выполнения. И этого нельзя избежать в PHP-Scoper без хаков.
Так в чём же решение? Очень просто: не ссылайтесь на WP_Query, не вызывайте get_posts и не используйте никакой код WordPress в кодовой базе, которая будет обрабатываться scoping.

Нет, я не сумасшедший, и уверен, что вы тоже. И да, я понимаю, что мы создаём плагин для WordPress... Позвольте объяснить.
Как можно не включать код WordPress? Разделив кодовую базу на 2 набора пакетов:
- Те, которые содержат код WordPress, не ссылаясь на код каких-либо внешних библиотек
- Те, которые содержат бизнес-логику, не содержа никакого кода WordPress, и включая все необходимые зависимости и ссылки на их код
Таким образом, вместо единой кодовой базы у нас есть несколько кодовых баз (или пакетов), где одни будут обрабатываться scoping, а другие нет, и все вместе они образуют плагин, связанный через Composer.
Тогда мы не применяем scoping к пакету, содержащему код WordPress, избегая конфликта. Это работает, потому что он не ссылается ни на какой код, принадлежащий внешним зависимостям. Все ссылки внутренние, например MyNamespace\MyPlugin\MyClass. Но их не нужно обрабатывать scoping, потому что мы можем смело предположить, что в сайте WordPress будет установлена только 1 версия плагина, и мы можем добавить наш namespace MyNamespace\* в whitelist.
Более того, если наш плагин может быть расширен, то добавление нашего собственного namespace в whitelist обязательно. Например, field resolver для Gato GraphQL реализуется путём расширения класса PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Если бы я применил к нему scoping, разработчики были бы вынуждены ссылаться на PoP\ComponentModel\FieldResolvers\AbstractFieldResolver для разработки и на PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver для production. Это недопустимо.
Тогда мы применяем scoping только к пакетам бизнес-логики, которые содержат ссылки на все внешние библиотеки, но не содержат кода WordPress.
Подводя итог, мы меняем эту стратегию:
«Иметь единую кодовую базу, применить к ней scoping, а затем с болью и огромным терпением устранять ущерб, молясь о том, чтобы ни один конфликт не остался незамеченным и не 💣 взорвался в production»
На такую:
«Разделите кодовую базу на 2 группы, примените scoping только к той, которая содержит ссылки на внешние зависимости и не содержит кода WordPress, и идите пить заслуженный дайкири 🍹».
Покажи настоящее дело
Пора вскрыть колбасу и посмотреть, есть ли там настоящее мясо 🌭.
4 дня назад у меня был следующий код в моём плагине:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use Parsedown;
class MarkdownContentParser
{
protected function getHTMLContent(string $fileContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Класс Parsedown поступает из внешней зависимости erusev/parsedown, как определено в composer.json плагина:
{
"require": {
"erusev/parsedown": "^1.7"
}
}Следовательно, мой плагин содержал ссылки на внешнюю библиотеку, поэтому мне нужно было применить к нему scoping, чтобы преобразовать Parsedown в PrefixedByPoP\Parsedown. Но при этом scoping был бы применён и ко всему коду WordPress в плагине, вызывая конфликты.
Поэтому я выделил код в отдельный пакет под названием graphql-api/markdown-convertor и заменил стороннюю зависимость в composer.json своей собственной зависимостью:
{
"require": {
"graphql-api/markdown-convertor": "^0.8"
}
}Теперь плагин избегает ссылки на внешнюю библиотеку; вместо этого он ссылается на сервис MarkdownConvertorInterface из нового пакета:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use GraphQLAPI\MarkdownConvertor\MarkdownConvertorInterface;
class MarkdownContentParser extends AbstractContentParser
{
protected MarkdownConvertorInterface $markdownConvertorInterface;
function __construct(MarkdownConvertorInterface $markdownConvertorInterface)
{
$this->markdownConvertorInterface = $markdownConvertorInterface;
}
protected function getHTMLContent(string $fileContent): string
{
return $this->markdownConvertorInterface->convertMarkdownToHTML($fileContent);
}
}Ссылка на стороннюю зависимость выполняется в новом пакете:
namespace GraphQLAPI\MarkdownConvertor;
use Parsedown;
class MarkdownConvertor implements MarkdownConvertorInterface
{
public function convertMarkdownToHTML(string $markdownContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Наконец, нам нужно:
- Применить scoping к зависимости
graphql-api/markdown-convertor - Пропустить scoping кода плагина
- Добавить namespace
GraphQLAPI\*в whitelist, чтобы мои собственные классы не попадали под scoping
Это и есть стратегия в целом. Отныне всё будет повторением этой же идеи — убирать все внешние зависимости из кода, пока, вуаля, плагин не станет пригодным для scoping.
Зависимости для извлечения — только те, что находятся в разделе require вашего файла composer.json; для require-dev вы можете оставить любые зависимости, внешние или нет, поскольку нам не нужно применять scoping к зависимостям, используемым для разработки; только те, которые нужны для создания и поставки плагина в production, должны быть обработаны.
В итоге composer.json вашего плагина не должен содержать никаких внешних зависимостей. Для моего плагина он выглядит вот так:
{
"require": {
"php": "^7.4|^8.0",
"getpop/engine-wp": "^0.8",
"graphql-api/markdown-convertor": "^0.8",
"graphql-by-pop/graphql-clients-for-wp": "^0.8",
"graphql-by-pop/graphql-endpoint-for-wp": "^0.8",
"graphql-by-pop/graphql-server": "^0.8",
"pop-schema/basic-directives": "^0.8",
"pop-schema/comment-mutations-wp": "^0.8",
"pop-schema/commentmeta-wp": "^0.8",
"pop-schema/comments-wp": "^0.8",
"pop-schema/custompost-mutations-wp": "^0.8",
"pop-schema/custompostmedia-mutations-wp": "^0.8",
"pop-schema/custompostmedia-wp": "^0.8",
"pop-schema/custompostmeta-wp": "^0.8",
"pop-schema/generic-customposts": "^0.8",
"pop-schema/media-wp": "^0.8",
"pop-schema/pages-wp": "^0.8",
"pop-schema/post-mutations": "^0.8",
"pop-schema/post-tags-wp": "^0.8",
"pop-schema/posts-wp": "^0.8",
"pop-schema/taxonomymeta-wp": "^0.8",
"pop-schema/taxonomyquery-wp": "^0.8",
"pop-schema/user-roles-access-control": "^0.8",
"pop-schema/user-roles-wp": "^0.8",
"pop-schema/user-state-mutations-wp": "^0.8",
"pop-schema/user-state-wp": "^0.8",
"pop-schema/usermeta-wp": "^0.8",
"pop-schema/users-wp": "^0.8"
}
}Все эти пакеты с namespace'ами getpop, graphql-api, graphql-by-pop и pop-schema — мои: зависимости, содержащие весь код плагина. Они распределены по разным namespace'ам для лучшего управления кодом, но в этом нет необходимости: использование единого namespace тоже прекрасно работает.
Теперь, по мере роста количества пакетов в вашем приложении, вам нужно будет хранить их все в монорепозитории, иначе вы сойдёте с ума, создавая pull request'ы, затрагивающие более одного пакета (поверьте, я через это прошёл). В моём случае все мои пакеты хранятся в монорепозитории GatoGraphQL/GatoGraphQL, и я синхронизирую их с помощью замечательного Monorepo Builder (нужно написать статью об этом инструменте — это настоящий спасательный круг!).
Namespace'ы этих пакетов — PoP, GraphQLAPI, GraphQLByPoP и PoPSchema. Поскольку они мои, я знаю, что они будут встречаться в приложении только один раз, и поэтому могу избежать их обработки scoping.
Для этого я добавляю их в whitelist в scoper.inc.php:
return [
'whitelist' => [
// Own namespaces
'PoPSchema\*',
'PoP\*',
'GraphQLByPoP\*',
'GraphQLAPI\*',
// Own container cache
'PoPContainer\*',
],
];Последняя запись соответствует контейнеру внедрения зависимостей, который тоже нужно обработать scoping. По умолчанию этому контейнеру присваивается имя ProjectServiceContainer, прямо в глобальном namespace. Но PHP-Scoper не поддерживает добавление в whitelist конкретных классов из глобального namespace. Поэтому я добавил искусственный namespace PoPContainer в whitelist и присвоил этот namespace при записи контейнера на диск:
$dumper = new PhpDumper($containerBuilder);
file_put_contents(
self::$cacheFile,
$dumper->dump(
// Save under own namespace to avoid conflicts
array('namespace' => 'PoPContainer')
)
);Вы можете заметить, что среди пакетов некоторые заканчиваются на -wp (например, pop-schema/users-wp), а другие нет (например, graphql-by-pop/graphql-server). Да, вы угадали: первые содержат код WordPress и не ссылаются на внешние библиотеки, а вторые могут содержать ссылки на внешние библиотеки, но не содержат кода WordPress вообще.
Тогда я пропускаю scoping пакетов WordPress:
return [
'finders' => [
// Scope packages under vendor/, excluding local WordPress packages
Finder::create()
->files()
->notPath([
// Exclude libraries ending in "-wp"
'#getpop/[a-zA-Z0-9_-]*-wp/#',
'#pop-schema/[a-zA-Z0-9_-]*-wp/#',
'#graphql-by-pop/[a-zA-Z0-9_-]*-wp/#',
])
->in('vendor')
]
];Что делать, если какому-то пакету WordPress нужно ссылаться на внешнюю библиотеку, и это нельзя вынести в другой пакет? Например, мой пакет getpop/routing-wp зависит от brain/cortex, и это неизбежно.
Я не могу применить scoping ко всему пакету, поскольку getpop/routing-wp содержит код WordPress. Вместо этого я определяю файлы, где такие ссылки присутствуют, и убеждаюсь, что они не содержат кода WordPress. Тогда я могу применить scoping только к этим файлам.
В данном случае ссылка на Cortex/Brain присутствует в 2 файлах, в том числе в layers/Engine/packages/routing-wp/src/Hooks/SetupCortexHookSet.php:
namespace PoP\RoutingWP\Hooks;
use PoP\Hooks\AbstractHookSet;
use Brain\Cortex\Route\RouteCollectionInterface;
use Brain\Cortex\Route\RouteInterface;
use Brain\Cortex\Route\QueryRoute;
use PoP\RoutingWP\WPQueries;
use PoP\Routing\Facades\RoutingManagerFacade;
class SetupCortexHookSet extends AbstractHookSet
{
protected function init()
{
$this->hooksAPI->addAction(
'cortex.routes',
[$this, 'setupCortex'],
1
);
}
/**
* @param RouteCollectionInterface<RouteInterface> $routes
*/
public function setupCortex(RouteCollectionInterface $routes): void
{
$routingManager = RoutingManagerFacade::getInstance();
foreach ($routingManager->getRoutes() as $route) {
$routes->addRoute(new QueryRoute(
$route,
function (array $matches) {
return WPQueries::STANDARD_NATURE;
}
));
}
}
}Замечаете странность? Это реализация хука, но add_action не вызывается, поскольку здесь не может быть кода WordPress. Вместо этого вызывается функция addAction сервиса HooksAPIInterface, а этот сервис реализован классом HooksAPI в пакете getpop/hooks-wp, где может быть код WordPress:
namespace PoP\HooksWP;
use PoP\Hooks\HooksAPIInterface;
class HooksAPI implements HooksAPIInterface
{
public function addAction(string $tag, callable $function_to_add, int $priority = 10, int $accepted_args = 1): void
{
add_action($tag, $function_to_add, $priority, $accepted_args);
}
}Теперь, когда код аккуратно разделён, мы можем применить scoping к этим 2 файлам, ссылающимся на внешние зависимости:
return [
'finders' => [
Finder::create()->append([
'vendor/getpop/routing-wp/src/Component.php',
'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
])
]
];Ранее я упоминал, что настройка scoping заняла несколько часов, но только после 2 месяцев работы. Что ж, этот пример наглядно демонстрирует, что я имел в виду: вся настоящая работа заключается в том, чтобы аккуратно разделить кодовую базу на 2 набора.
В моём случае работа заняла 2 месяца, потому что степень детализации была чрезвычайной: плагин превратился в композицию из 125 пакетов! Но это исключительный случай, цель которого — сделать базовый сервер плагина CMS-agnostic, чтобы поддерживать реализацию для других CMS/фреймворков, просто переимплементировав соответствующие пакеты -wp.
(Я подробно написал об этой стратегии в статьях Abstracting WordPress Code To Reuse With Other CMSs: Concepts и Implementation.)
Это, безусловно, немалая работа, но улучшенная чистота кода того стоит. И не только для применения scoping к плагину, что стало для меня полной неожиданностью — я до сих пор радуюсь этому негаданному счастью. Например, я запускаю PHPStan и PHPUnit отдельно для кода WordPress и не-WordPress, что избавляет меня от многих головных болей.
Как только кодовая база приведена в порядок, мир внезапно становится значительно лучше.
Тестирование
Так как же мы тестируем эту махину?
Решение, к которому я пришёл, — опираться на Rector, тот же инструмент, который я использую для понижения версии кода с PHP 7.4 для разработки до 7.1 для production.
Идея такова:
- Применить scoping к плагину
- Проанализировать его с помощью Rector, применив любое правило (неважно какое)
Если при применении scoping что-то пошло не так, Rector не сможет загрузить какой-либо класс и выдаст ошибку. Например, если класс Brain\Cortex был обработан scoping как PrefixedByPoP\Brain\Cortex, но какая-то ссылка на него осталась как Brain\Cortex, автозагрузка этого класса завершится ошибкой.
Вот моя GitHub Action для тестирования (используется working-directory, поскольку я работаю из корня монорепозитория, но scoping происходит в папке плагина):
name: Scope Gato GraphQL tests
on:
push:
branches:
- master
pull_request: null
env:
COMPOSER_ROOT_VERSION: "dev-master"
jobs:
main:
defaults:
run:
working-directory: layers/GraphQLAPIForWP/plugins/graphql-api-for-wp
name: Scope the plugin code via PHP-Scoper, and execute tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Set-up PHP
uses: shivammathur/setup-php@v2
with:
php-version: 7.4
coverage: none
env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Install root dependencies
uses: "ramsey/composer-install@v1"
- name: Install plugin dependencies for PROD
run: composer install --no-dev --no-progress --no-interaction --ansi
- name: Install PHP-Scoper
run: |
composer global config minimum-stability dev
composer global config prefer-stable true
composer global require humbug/php-scoper
# The scoped results correspond to vendor/, so must generate them in such folder
- name: Scope plugin into separate folder
run: php-scoper add-prefix --output-dir ../../../../build-prefixed/vendor --ansi
- name: Copy scoped code back into plugin
run: rsync -av build-prefixed/ layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/ --quiet
working-directory: .
- name: Regenerate autoloader
run: composer dumpautoload --optimize --classmap-authoritative --ansi
- name: Run Rector on the scoped code
run: vendor/bin/rector process --config=layers/GraphQLAPIForWP/plugins/graphql-api-for-wp/rector-test-scoping.php --ansi
working-directory: .
И вот моя конфигурация Rector:
use Rector\CodeQuality\Rector\LogicalAnd\AndAssignsToSeparateLinesRector;
use Rector\Core\Configuration\Option;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;
return static function (ContainerConfigurator $containerConfigurator): void {
$services = $containerConfigurator->services();
$services->set(AndAssignsToSeparateLinesRector::class);
$parameters->set(Option::AUTO_IMPORT_NAMES, true);
$parameters->set(Option::AUTOLOAD_PATHS, [
__DIR__ . '/vendor/scoper-autoload.php',
__DIR__ . '/vendor/erusev/parsedown/Parsedown.php',
__DIR__ . '/vendor/jrfnl/php-cast-to-type/cast-to-type.php',
__DIR__ . '/vendor/jrfnl/php-cast-to-type/class.cast-to-type.php',
]);
// files to rector
$parameters->set(Option::PATHS, [
__DIR__ . '/vendor',
]);
// files to skip
$parameters->set(Option::SKIP, [
// Exclude tests
'*/tests/*',
__DIR__ . '/vendor/nikic/fast-route/test/*',
__DIR__ . '/vendor/psr/log/Psr/Log/Test/*',
__DIR__ . '/vendor/symfony/service-contracts/Test/*',
]);
};Вы можете заметить, что некоторые файлы зависимостей, например erusev/parsedown/Parsedown.php, нужно добавить в Option::AUTOLOAD_PATHS. Это связано с тем, что обработка scoping для composer.json пакета не на 100% надёжна, и тогда его автозагрузка может завершиться сбоем.
Когда это происходит, Rector будет жаловаться на то, что автозагрузка какого-то класса завершилась неудачей. После этого мы определяем соответствующий файл и вручную добавляем его в пути автозагрузки.
Посмотрите на результаты
Вот исходный код плагина, а вот его версия со scoping (и с понижением до PHP 7.1).
Найдите 7 отличий 😁. (Подсказка: ищите PrefixedByPoP.)
А вот итоговый файл плагина graphql-api.zip, готовый к установке на ваш сайт.
Вот и всё. Надеюсь, это было полезно 😃💪🚀