🍾 Gato GraphQL jest teraz scoped, dzięki PHP-Scoper!
Wtyczka Gato GraphQL jest teraz scoped. Oznacza to, że wtyczka może wreszcie zostać przesłana do katalogu wtyczek WordPress.

Aby to osiągnąć, używam wspaniałego PHP-Scoper. Korzystanie z tej biblioteki z WordPress nie jest pozbawione wyzwań, dlatego w tym wpisie wyjaśnię, jak udało mi się to rozwiązać.
Sekcje:
- Podjęcie decyzji o nadaniu scope
- Sprawdzanie dostępnych opcji
- Próba z Mozart, zakończona niepowodzeniem
- Poznawanie PHP-Scoper i ucieczka w panice
- Powrót do PHP-Scoper, tym razem na poważnie
- PHP-Scoper, łatwy sposób 😎 👈🏽 Tutaj zaczyna się moje rozwiązanie
- Pokaż mi prawdziwe rzeczy
- Testowanie
- Sprawdź wyniki
Podjęcie decyzji o nadaniu scope
Kilka tygodni temu Matt Mullenweg ogłosił, że będzie pilnie obserwował "wtyczkę GraphQL", wyraźnie mając na myśli WPGraphQL. Jego wypowiedź pokazuje, że uważa, iż istnieje tylko jedna wtyczka GraphQL, podczas gdy w rzeczywistości są dwie (ta pominięta to, cóż, moja). To uświadomiło mi, jak mała jest widoczność mojej wtyczki, i poczułem się z tym źle.
Matt nie wiedział, że moja wtyczka istnieje. Podobnie jak większość społeczności WordPress. Wyraźnie nie promuję jej wystarczająco dobrze. Wiem, że jestem kiepski w marketingu i mediach społecznościowych; radzę sobie tylko ze sprawami technicznymi (przynajmniej tak mi się wydaje). Postanowiłem więc coś z tym zrobić, przynajmniej w granicach moich możliwości.
Oto nad czym pracuję:
- Właśnie skończyłem kodować tę samą stronę, gatographql.com, i uruchomiłem ją 2 tygodnie temu (hura! 🥳 Przy okazji, co o niej sądzisz? Zapraszam do podzielenia się opinią przez DM lub email)
- 3 dni temu w końcu zacząłem nadawać scope wtyczce i ukończyłem to zadanie wczoraj! (O 3 w nocy, ale było warto 😅)
- A na koniec, już pracuję nad nadchodzącą wersją
0.8, która będzie pierwszą dostępną w repozytorium wtyczek
Nadanie scope wtyczce jest obowiązkowe, aby ją przesłać do repozytorium, ponieważ inaczej mogłaby kolidować z inną wtyczką, która wymaga tej samej zależności co moja, ale w innej wersji. Osiągnięcie tego jest naprawdę ważnym kamieniem milowym; żaden inny etap prac nie jest tak istotny. Na przykład muszę jeszcze uzupełnić schemat GraphQL, aby w pełni odpowiadał modelowi danych WordPress, ale to będzie realizowane stopniowo w każdej nowej wersji.
Tak więc za kilka tygodni wtyczka pojawi się w wynikach wyszukiwania dla "GraphQL", a osoby, które faktycznie potrzebują wdrożyć GraphQL API, poznają istnienie mojej wtyczki.
Rzeczywiście chcę, aby moja wtyczka była poważnie brana pod uwagę w przyszłości WordPress. Pracuję nad nią od kilku lat. Repozytorium zostało założone w sierpniu 2016 roku; to nawet przed powstaniem WPGraphQL i na początku istnienia GraphQL. Ale nie wiedziałem, że projekt stanie się serwerem GraphQL; ten kierunek obrał dopiero około 1,5 roku temu.
(Projekt jest właściwie frameworkiem do budowania aplikacji przy użyciu komponentów po stronie serwera, a serwer GraphQL mógł być doskonale zbudowany przy użyciu tej architektury. Więc po prostu go zbudowałem).
WPGraphQL to ugruntowana wtyczka, i słusznie: została uruchomiona kilka lat temu, a wokół niej zbudowała się społeczność. Praca Jasona Bahla (zatrudnionego przez Gatsby) i współpracowników jego projektu była wybitna: integracja WordPress z Jamstack jest teraz łatwiejsza niż kiedykolwiek.
Ale jedno to Gatsby i Jamstack, a drugie to WordPress. WordPress stanowi 40% sieci, nie tylko dane wejściowe dla generatora statycznych stron.
Teraz możemy więc rozważyć, czy WPGraphQL jest właściwą opcją, bez konieczności podejmowania tej decyzji za nas z braku alternatyw. Możemy teraz przeanalizować obie wtyczki, aby sprawdzić, której cele są bardziej zgodne z tym, co jest ważne dla WordPress.
Gato GraphQL może również współpracować z Jamstack. Ale jego główne cele są, uważam, ambitniejsze: "demokratyzacja publikowania danych", tak aby edytowanie API stało się tak proste jak edytowanie wpisu (coś, co każdy może zrobić), i uczynienie WordPress systemem operacyjnym sieci.
Gdy wtyczka będzie dostępna w repozytorium, mam nadzieję, że więcej osób ją wypróbuje i powie "Hej, to jest niesamowite! Jak to możliwe, że nie wiedziałem o tym wcześniej?".
A wtedy wybór "wtyczki GraphQL" nie będzie z góry przesądzony, a społeczność WordPress będzie mogła rozważyć zarówno WPGraphQL, jak i Gato GraphQL na podstawie ich własnych zalet.
Skoro wyjaśniłem już swoje motywacje, porozmawiajmy o sprawach technicznych 🤓.
Sprawdzanie dostępnych opcji
Nadanie scope wtyczce wiąże się z uruchomieniem pewnych narzędzi, które przyjmują kod wtyczki jako dane wejściowe i produkują wtyczkę ze scope. Nic wielkiego, prawda? Jak trudne to może być?

Cóż, w zależności od codebase, samo wykonanie polecenia scope może nie wystarczyć. Następnie musimy sprawdzić błędy w konsoli, naprawić je, dokładnie przetestować aplikację, zidentyfikować błędy i przyczyny ich powstawania, naprawić je i iterować. Aby zrobić to poprawnie, może to wymagać trochę czasu.
Istnieją 2 biblioteki do nadawania scope, które mają różne cele:
- Mozart, dla kodu WordPress
- PHP-Scoper, dla dowolnego kodu PHP, szczególnie przy tworzeniu PHARów
Ponieważ mam wtyczkę WordPress, najpierw wypróbowałem Mozart. Zobaczmy, jak to poszło.
Próba z Mozart, zakończona niepowodzeniem
Próbowałem Mozart około 1 roku temu. Według dokumentacji "polecenie mozart compose robi całą magię". Spodziewałem się więc, że wszystko będzie bardzo szybkie i proste, i że będę mógł cieszyć się resztą dnia.
Niestety Mozart nigdy nie działał dla mojej codebase. Ciągle napotykał problemy, więc scope nigdy się nie zmaterializował. Nie mogłem też uzyskać potrzebnej pomocy: złożyłem PR, ale nie był brany pod uwagę do scalenia, i nie zostałem nawet o tym powiadomiony, więc czekałem, aż naturalnie straciłem zainteresowanie tym projektem.
Uważam, że Mozart nie radził sobie z niektórymi zależnościami mojej wtyczki. Korzystam z kilku komponentów Symfony, w tym DependencyInjection, Cache i Dotenv, z wszystkim zarządzanym przez Composer.
Nadanie scope PHP to nie tylko kwestia PHP, więc narzędzie do scope ma wiele przeszkód do ominięcia i wyzwań do rozwiązania. Na przykład Symfony DependencyInjection używa plików YAML do konfiguracji, które też muszą otrzymać scope. A plik composer.json zawiera konfigurację dla automatycznego ładowania PSR-4, który też musi otrzymać scope. I, jak sądzę, Mozart nie radził sobie z tymi złożonościami poprawnie.
Jestem jednak pewien, że moje doświadczenie nie jest jedyne, i że jest wielu zadowolonych użytkowników. Ponadto moja nieudana próba miała miejsce rok temu, więc zastanawiam się, czy narzędzie zostało od tamtej pory ulepszone. A poza tym nie zapomnij o powiedzeniu: "Wszystkie wtyczki ze scope są do siebie podobne; każda wtyczka bez scope jest bez scope na swój własny sposób", więc możliwe, że zawodzi tylko dla mnie.
Jeśli twoja wtyczka WordPress jest prosta, z logiką samodzielnie zawartą, a scope musi być realizowane wyłącznie w kodzie PHP, to istnieje szansa, że Mozart zadziała. Musisz to po prostu sprawdzić.
Poznawanie PHP-Scoper i ucieczka w panice
Zwróciłem się więc ku PHP-Scoper. Jednak nigdy nawet nie próbowałem go wypróbować, bo od razu mnie przestraszyło.
Na początku to narzędzie nie obsługuje WordPress natywnie. A poza tym zalecają zapoznanie się z ich własnym Makefile, który wygląda tak:
# 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-versionI jeszcze 600 linii, wszystkie takie same. Wygląda jak zagadka. Przekonanie, że muszę zrozumieć ten kod tylko po to, by nadać scope mojej wtyczce, sprawiło, że uciekłem bez ceremonii.
(Cóż, zrozumienie tego kodu to ich zalecenie do testowania aplikacji ze scope, ale nie jest wymagane. Możemy też po prostu uruchomić polecenie php-scoper add-prefix, pozwolić mu zrobić całą magię, i pójść na nasz drink.)
Powrót do PHP-Scoper, tym razem na poważnie
Tak więc 3 dni temu podjąłem decyzję o wdrożeniu scope, jakoś. Musiałem to zrealizować.
Wróciłem do PHP-Scoper, aby go poważnie wypróbować. Wiedziałem, że WordPress może być z nim objęty scope po przeczytaniu PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (autorstwa błyskotliwych osób z Delicious Brains). To była tylko kwestia podejścia i wytrwałości.
Zbadałem kilka istniejących rozwiązań, w tym:
Ale wszystkie wydały mi się nie w pełni satysfakcjonujące: albo kod wygląda na hacky, albo jest kruchy i czeka na zepsucie w pewnym momencie.
Na przykład wtyczka Google Web Stories nadaje scope kodowi, a następnie cofa każdy z konfliktów:
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 );
// ...
}
]
]Rozumiem, dlaczego to robią, ale mi się to nie podoba. Za każdym razem, gdy jakaś nowa funkcja WordPress jest odwołana, muszą upewnić się, że trafia też na tę listę. Jest to zbyt manualne, zbyt kruche.
Takie było więc moje wyzwanie: czy nie ma prostszego sposobu na nadanie scope wtyczce, opierając się na kodzie, który możemy pokazać przyjaciołom i współpracownikom bez wstydu?
PHP-Scoper, łatwy sposób 😎
Okazało się to prostsze niż myślałem! W zaledwie kilka godzin miałem wszystko działające.

Kiedy mówię "łatwy" i "godziny", właściwie mam na myśli: wszystko zadziałało od razu, ale dopiero po spędzeniu 2 miesięcy na tworzeniu odpowiedniej struktury dla codebase (wyjaśnię to lepiej później).
Ale ważne jest to: jeśli masz odpowiednią konfigurację projektu, nadanie scope może być zrealizowane w krótkim czasie.
Problem z nadawaniem scope kodowi WordPress tkwi, cóż, w kodzie WordPress. Problem jest wyjaśniony tutaj, ale sprowadza się do tego, że wszystkie funkcje i klasy WordPress też otrzymują namespace. Jeśli więc odwołamy się do WP_Query lub wywołamy get_posts w naszym kodzie, zostaną one przekształcone w MyPrefixedNamespace\WP_Query i MyPrefixedNamespace\get_posts, powodując epicki błąd w czasie wykonania. I tego nie można uniknąć w PHP-Scoper bez hacków.
Jakie jest więc rozwiązanie? Proste: nie odwołuj się do WP_Query, nie wywołuj get_posts, i nie używaj żadnego kodu WordPress w codebase która otrzyma scope.

Nie, nie zwariowałem i jestem pewien, że ty też nie. I tak, wiem, że budujemy wtyczkę WordPress... Pozwól, że wyjaśnię.
Jak możemy nie uwzględniać kodu WordPress? Dzieląc codebase na 2 zestawy pakietów:
- Te zawierające kod WordPress, bez odwoływania się do kodu z jakiejkolwiek zewnętrznej biblioteki
- Te zawierające logikę biznesową, bez zawierania jakiegokolwiek kodu WordPress, i zawierające wszystkie wymagane zależności i odwołania do ich kodu
W ten sposób zamiast mieć jedną codebase, mamy wiele codebase (lub pakietów), gdzie niektóre otrzymają scope, a inne nie, i wszystkie tworzą wtyczkę, połączone ze sobą przez Composer.
Następnie nie nadajemy scope pakietowi zawierającemu kod WordPress, unikając konfliktu. Działa to, ponieważ nie odwołuje się do żadnego kodu należącego do jakiejkolwiek zewnętrznej zależności. Wszystkie odwołania są wewnętrzne, takie jak MyNamespace\MyPlugin\MyClass. Ale te nie muszą otrzymywać scope, ponieważ możemy bezpiecznie założyć, że w witrynie WordPress będzie zainstalowana tylko 1 wersja wtyczki, i możemy umieścić nasz namespace MyNamespace\* na białej liście.
Ponadto, jeśli nasza wtyczka może być rozszerzana, umieszczenie naszego własnego namespace na białej liście jest obowiązkowe. Na przykład, resolver pola dla Gato GraphQL jest implementowany przez rozszerzenie klasy PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Gdybym nadał mu scope, programiści byliby zmuszeni odwoływać się do PoP\ComponentModel\FieldResolvers\AbstractFieldResolver podczas tworzenia i do PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver w produkcji. To jest nie do przyjęcia.
Następnie nadajemy scope tylko pakietom z logiką biznesową, które zawierają odwołania do wszystkich zewnętrznych bibliotek, ale nie zawierają kodu WordPress.
Podsumowując, zmieniamy tę strategię:
"Mieć jedną codebase, nadać jej scope, a następnie boleśnie i z dużą cierpliwością cofać szkody, modląc się, aby żaden konflikt nie przeszedł niezauważony i nie 💣 wybuchł w produkcji"
Na tę:
"Podziel codebase na 2 grupy, nadaj scope tylko tej zawierającej odwołania do zewnętrznych zależności i nie zawierającej kodu WordPress, i idź na zasłużony drink 🍹".
Pokaż mi prawdziwe rzeczy
Czas otworzyć kiełbasę i sprawdzić, czy jest w niej prawdziwe mięso 🌭.
4 dni temu miałem następujący kod w mojej wtyczce:
namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
use Parsedown;
class MarkdownContentParser
{
protected function getHTMLContent(string $fileContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Klasa Parsedown pochodzi z zewnętrznej zależności erusev/parsedown, zgodnie z definicją w composer.json wtyczki:
{
"require": {
"erusev/parsedown": "^1.7"
}
}Moja wtyczka zawierała więc odwołania do zewnętrznej biblioteki, dlatego musiałem nadać jej scope, aby przekształcić Parsedown w PrefixedByPoP\Parsedown. Ale zrobienie tego nadałoby scope również całemu kodowi WordPress w wtyczce, powodując konflikty.
Wyodrębniłem więc kod do oddzielnego pakietu, nazwanego graphql-api/markdown-convertor, i zastąpiłem zależność zewnętrzną w composer.json moją własną zależnością:
{
"require": {
"graphql-api/markdown-convertor": "^0.8"
}
}Teraz wtyczka unika odwoływania się do zewnętrznej biblioteki; zamiast tego odwołuje się do serwisu MarkdownConvertorInterface z nowego pakietu:
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);
}
}Odwołanie do zależności zewnętrznej jest realizowane w nowym pakiecie:
namespace GraphQLAPI\MarkdownConvertor;
use Parsedown;
class MarkdownConvertor implements MarkdownConvertorInterface
{
public function convertMarkdownToHTML(string $markdownContent): string
{
return (new Parsedown())->text($markdownContent);
}
}Na koniec musimy:
- Nadać scope zależności
graphql-api/markdown-convertor - Pominąć nadawanie scope kodu wtyczki
- Umieścić namespace
GraphQLAPI\*na białej liście, aby uniknąć nadawania scope moim własnym klasom
To jest w zasadzie cała strategia. Od tej pory będzie to powtórzenie tej samej idei, aby usunąć wszystkie zewnętrzne zależności z kodu, aż voilà, wtyczka może otrzymać scope.
Zależności do wyodrębnienia to tylko te z sekcji require w pliku composer.json; dla require-dev możesz zachować dowolną zależność, zewnętrzną lub nie, ponieważ nie musimy nadawać scope zależnościom używanym do tworzenia oprogramowania; tylko te używane do tworzenia i dystrybucji wtyczki, dla produkcji, muszą otrzymać scope.
Na końcu composer.json twojej wtyczki nie powinien zawierać żadnych zewnętrznych zależności. Dla mojej wtyczki wygląda to tak:
{
"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"
}
}Wszystkie te pakiety, z namespace getpop, graphql-api, graphql-by-pop i pop-schema, są moje: zależności zawierające cały kod wtyczki. Są rozdzielone na różne namespace, aby lepiej zarządzać kodem, ale nie musisz tego robić: używanie jednego namespace działa dobrze.
Teraz, gdy liczba pakietów w twojej aplikacji rośnie, będziesz musiał hostować je wszystkie w monorepo, albo oszalejesz tworząc pull requesty obejmujące więcej niż jeden pakiet (uwierz mi, byłem tam). W moim przypadku wszystkie moje pakiety są hostowane w monorepo GatoGraphQL/GatoGraphQL, i utrzymuję je zsynchronizowane dzięki wspaniałemu Monorepo Builder (muszę napisać artykuł o tym narzędziu, to prawdziwy ratunek!).
Namespace dla tych pakietów to PoP, GraphQLAPI, GraphQLByPoP i PoPSchema. Ponieważ są moje, wiem, że pojawią się tylko raz w aplikacji, i mogę uniknąć nadawania im scope.
Aby to zrobić, umieszczam je na białej liście w scoper.inc.php:
return [
'whitelist' => [
// Own namespaces
'PoPSchema\*',
'PoP\*',
'GraphQLByPoP\*',
'GraphQLAPI\*',
// Own container cache
'PoPContainer\*',
],
];Ostatni wpis odpowiada kontenerowi wstrzykiwania zależności, który też musi otrzymać scope. Domyślnie ten kontener otrzymuje nazwę ProjectServiceContainer, bezpośrednio w globalnym namespace. Ale PHP-Scoper nie obsługuje umieszczania na białej liście konkretnych klas z globalnego namespace. Dlatego dodałem sztuczny namespace PoPContainer do białej listy i przypisałem ten namespace podczas zapisywania kontenera na dysk:
$dumper = new PhpDumper($containerBuilder);
file_put_contents(
self::$cacheFile,
$dumper->dump(
// Save under own namespace to avoid conflicts
array('namespace' => 'PoPContainer')
)
);Możesz zauważyć, że w odniesieniu do pakietów, niektóre kończą się na -wp (jak pop-schema/users-wp), a inne nie (jak graphql-by-pop/graphql-server). Tak, zgadłeś: te pierwsze zawierają kod WordPress i nie odwołują się do żadnych zewnętrznych bibliotek, a te drugie mogą zawierać odwołania do zewnętrznych bibliotek, ale nie zawierają żadnego kodu WordPress.
Następnie pomijam nadawanie scope pakietom 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')
]
];Co się dzieje, jeśli jakiś pakiet WordPress musi odwoływać się do zewnętrznej biblioteki i nie można tego wyodrębnić do innego pakietu? Na przykład mój pakiet getpop/routing-wp zależy od brain/cortex, i to jest nieuniknione.
Nie mogę nadać scope całemu pakietowi, ponieważ getpop/routing-wp zawiera kod WordPress. Zamiast tego identyfikuję pliki, w których te odwołania są wykonywane, i upewniam się, że nie zawierają żadnego kodu WordPress. Następnie mogę nadać scope tylko tym plikom.
W tym przypadku odwołanie do Cortex/Brain jest realizowane w 2 plikach, w tym 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;
}
));
}
}
}Zauważasz tu dziwność? To jest implementacja hooka, ale żaden add_action nie jest wywoływany, ponieważ nie mogę mieć tu kodu WordPress. Zamiast tego wywołuje funkcję addAction z serwisu HooksAPIInterface, a ten serwis jest implementowany przez klasę HooksAPI w pakiecie getpop/hooks-wp, gdzie możemy mieć kod 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);
}
}Teraz, gdy kod jest starannie podzielony, możemy nadać scope tym 2 plikom odwołującym się do zewnętrznych zależności:
return [
'finders' => [
Finder::create()->append([
'vendor/getpop/routing-wp/src/Component.php',
'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
])
]
];Wcześniej wspomniałem, że konfiguracja scope zajęła kilka godzin, ale dopiero po 2 miesiącach pracy. Cóż, ten przykład pokazuje, co miałem na myśli: prawdziwa praca polega na czystym podzieleniu codebase na 2 zestawy.
W moim przypadku praca zajęła 2 miesiące, ponieważ poziom szczegółowości był ekstremalny: wtyczka stała się kompozycją 125 pakietów! Ale to jest wyjątkowy przypadek, z celem, aby serwer bazowy wtyczki był agnostyczny względem CMS, aby obsługiwał implementację dla innych CMSów/frameworków, jedynie reimplementując odpowiednie pakiety -wp.
(Napisałem szczegółowo o tej strategii w artykułach Abstracting WordPress Code To Reuse With Other CMSs: Concepts i Implementation.)
To z pewnością sporo pracy, ale zwiększona czystość kodu jest tego warta. I nie tylko dla nadania scope wtyczce, co było dla mnie całkowitym zaskoczeniem i nadal cieszę się z tej nieoczekiwanej radości. Na przykład uruchamiam PHPStan i PHPUnit oddzielnie na kodzie WordPress i nie-WordPress, co oszczędza mi wielu bólów głowy.
Gdy codebase jest uporządkowana, świat nagle staje się o wiele lepszym miejscem.
Testowanie
Jak więc testujemy tę bestię?
Rozwiązanie, które wymyśliłem, polega na użyciu Rector, tego samego narzędzia, którego używam do obniżania wersji kodu z PHP 7.4, dla rozwoju, do 7.1, dla produkcji.
Idea jest następująca:
- Nadać scope wtyczce
- Przeanalizować ją za pomocą Rector, stosując dowolną regułę (nie ma znaczenia którą)
Jeśli coś poszło nie tak podczas nadawania scope, Rector nie będzie mógł załadować jakiejś klasy i wyrzuci błąd. Na przykład, jeśli klasa Brain\Cortex otrzymała scope jako PrefixedByPoP\Brain\Cortex, ale jakieś odwołanie do niej zostało zachowane jako Brain\Cortex, wtedy automatyczne ładowanie tej klasy zawiedzie.
Oto moja GitHub Action do testowania (working-directory jest używane, ponieważ operuję z korzenia monorepo, ale nadawanie scope odbywa się w folderze wtyczki):
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: .
I oto moja konfiguracja 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/*',
]);
};Możesz zauważyć, że niektóre pliki zależności, takie jak erusev/parsedown/Parsedown.php', muszą być dodane do Option::AUTOLOAD_PATHS. Dzieje się tak dlatego, że nadawanie scope plikowi composer.json pakietu nie jest w 100% niezawodne, i wtedy ich automatyczne ładowanie może zawieść.
Gdy to się zdarzy, Rector poskarży się, że jakieś klasy nie zostały załadowane automatycznie. Stamtąd identyfikujemy odpowiedni plik i ręcznie dodajemy go do ścieżek automatycznego ładowania.
Sprawdź wyniki
To jest kod źródłowy wtyczki, i to jest jej wersja ze scope (i z obniżoną wersją do PHP 7.1).
Znajdź 7 różnic 😁. (Daję ci podpowiedź: szukaj PrefixedByPoP.)
I to jest finalny plik wtyczki graphql-api.zip, gotowy do zainstalowania na twojej stronie.
To wszystko. Mam nadzieję, że to było przydatne 😃💪🚀