Blog

🍾 Gato GraphQL is nu scoped, dankzij PHP-Scoper!

Leonardo Losoviz
Door Leonardo Losoviz ·

Plugin Gato GraphQL is nu scoped. Dit betekent dat de plugin eindelijk geüpload kan worden naar de WordPress plugin-directory.

Zakelijk gesprek

Daarvoor gebruik ik de geweldige PHP-Scoper. Het gebruik van deze library met WordPress gaat niet zonder uitdagingen, dus ik leg in deze blogpost uit hoe ik het voor elkaar heb gekregen.

Secties:

De beslissing nemen om te scopen

Een paar weken geleden kondigde Matt Mullenweg aan dat hij de "GraphQL plugin" in de gaten zou houden, duidelijk verwijzend naar WPGraphQL. Zijn uitspraak toont aan dat hij gelooft dat er maar één GraphQL-plugin is, terwijl er in werkelijkheid twee zijn (de plugin die buiten beeld valt is, nou ja, de mijne). Dat deed me beseffen hoe weinig zichtbaarheid mijn plugin heeft, en ik voelde me er slecht over.

Matt wist niet dat mijn plugin bestond. De meeste van de WordPress-community ook niet, trouwens. Ik promoot hem duidelijk niet goed genoeg. Ik weet dat ik slecht ben in marketing en social media; ik ben gewoon redelijk met technische dingen (of zo geloof ik). Dus besloot ik er iets aan te doen, althans binnen mijn mogelijkheden.

Dit is waar ik aan werk:

  • Ik heb net het coderen van deze website, gatographql.com, afgerond en hem 2 weken geleden gelanceerd (joepie! 🥳 Trouwens, wat vind je ervan? Je bent van harte welkom om me feedback te geven, via DM of e-mail)
  • 3 dagen geleden ben ik eindelijk begonnen met het scopen van de plugin, en ik heb deze taak gisteren afgerond! (Om 3 uur 's nachts, maar het was het waard 😅)
  • En tot slot werk ik al aan de aankomende versie 0.8, die de eerste zal zijn die beschikbaar is in de plugin-repository

Het scopen van de plugin is verplicht om hem naar de repository te uploaden, want anders kan hij conflicteren met een andere plugin die dezelfde afhankelijkheid nodig heeft als mijn plugin, maar in een andere versie. Dit bereikt hebben is echt een belangrijke mijlpaal; geen andere ontwikkeling is zo belangrijk. Ik moet bijvoorbeeld nog het GraphQL-schema voltooien om volledig overeen te komen met het WordPress-datamodel, maar dat zal gestaag worden gedaan in elke nieuwe release.

Dus over een paar weken zal de plugin verschijnen bij het zoeken naar "GraphQL", en de mensen die daadwerkelijk een GraphQL API moeten implementeren zullen kennis maken met het bestaan van mijn plugin.

Ik wil inderdaad dat mijn plugin serieus wordt overwogen voor de toekomst van WordPress. Ik werk er al jaren aan. De repository werd gestart in augustus 2016; dat is zelfs voor WPGraphQL bestond, en aan het begin van GraphQL. Maar ik wist niet dat het project een GraphQL-server zou worden; die richting nam het pas ongeveer 1,5 jaar geleden.

(Het project is eigenlijk een framework om applicaties te bouwen met server-side componenten, en een GraphQL-server kon perfect worden gebouwd met deze architectuur. Dus heb ik hem gewoon gebouwd.)

WPGraphQL is een gevestigde plugin, en terecht: hij werd een paar jaar geleden gestart en er is een community omheen gebouwd. Het werk van Jason Bahl (die in dienst is bij Gatsby) en de bijdragers aan zijn project is uitstekend geweest: WordPress integreren in de Jamstack is nu eenvoudiger dan ooit.

Maar het ene is Gatsby en de Jamstack, het andere is WordPress. WordPress is 40% van het web, niet alleen een invoer voor een statische site-generator.

Nu kunnen we dus overwegen of WPGraphQL de juiste optie is, zonder dat deze beslissing voor ons wordt genomen bij gebrek aan alternatieven. We kunnen nu beide plugins analyseren om te zien wiens doelen meer in lijn zijn met wat belangrijk is voor WordPress.

Gato GraphQL kan ook werken met de Jamstack. Maar de belangrijkste doelstellingen zijn, naar mijn mening, grootser: "datapublicatie democratiseren", zodat het bewerken van een API even eenvoudig wordt als het bewerken van een bericht (iets wat iedereen kan doen), en WordPress de OS van het web laten worden.

Zodra de plugin beschikbaar is in de repository, hoop ik dat meer mensen hem uitproberen en zeggen "Hé, dit is geweldig! Hoe kan het dat ik dit eerder niet kende?".

En dan is de keuze van "de GraphQL-plugin" niet vooraf bepaald, en kan de WordPress-community zowel WPGraphQL als Gato GraphQL overwegen op basis van hun eigen verdiensten.

Nu mijn motivaties duidelijk zijn, laten we het over technische zaken hebben 🤓.

De opties bekijken

Een plugin scopen houdt in dat je wat tooling uitvoert, die de plugincode als invoer neemt en de gescopede plugin uitspuugt. Geen groot probleem, toch? Hoe moeilijk kan dat zijn?

Technisch gesprek

Welnu, afhankelijk van de codebase zal alleen het uitvoeren van het scope-commando niet genoeg zijn. Daarna moeten we fouten in de console controleren, ze oplossen, de applicatie grondig testen, fouten en hun oorzaken identificeren, ze oplossen, en itereren. Om het helemaal goed te krijgen, kan het wat tijd vergen.

Er zijn 2 libraries voor scoping, die verschillende doelen hebben:

  • Mozart, voor WordPress-code
  • PHP-Scoper, voor elke PHP-code, met name bij het produceren van PHARs

Omdat ik een WordPress-plugin heb, probeerde ik Mozart als eerste. Laten we kijken hoe dat ging.

Mozart uitproberen, en mislukken

Ik heb Mozart ongeveer 1 jaar geleden geprobeerd. Volgens de documentatie "doet het commando mozart compose alle magie". Ik verwachtte dus dat alles heel snel en eenvoudig zou zijn, en dat ik de rest van de dag kon genieten.

Helaas heeft Mozart nooit gewerkt voor mijn codebase. Het bleef problemen tegenkomen, dus de scoping kwam er nooit van. En ik kon de benodigde hulp niet krijgen: ik diende een PR in, maar die werd niet overwogen voor samenvoeging, en ik werd er niet eens over ingelicht, dus bleef ik wachten tot ik van nature mijn interesse in dit project verloor.

Ik geloof dat Mozart niet kon omgaan met sommige afhankelijkheden in mijn plugin. Ik gebruik meerdere Symfony-componenten, waaronder DependencyInjection, Cache en Dotenv, met alles beheerd via Composer.

PHP scopen gaat niet alleen over PHP, dus de scoper heeft veel hindernissen te vermijden en uitdagingen te overwinnen. Zo gebruikt Symfony DependencyInjection YAML-bestanden voor configuratie, en die moeten ook worden gescoopt. En het composer.json-bestand bevat de configuratie voor PSR-4-autoloading, en die moet ook worden gescoopt. En, naar mijn mening, kon Mozart deze complexiteiten niet goed aan.

Maar ik ben er zeker van dat mijn ervaring niet de enige is, en dat er veel tevreden gebruikers zijn. Ook vond mijn mislukte poging 1 jaar geleden plaats, dus ik vraag me af of de tool sindsdien is verbeterd. En vergeet dan de uitspraak niet: "Alle gescopede plugins lijken op elkaar; elke ongescopede plugin is ongescoopt op zijn eigen manier", dus misschien mislukt het alleen voor mij.

Als je WordPress-plugin eenvoudig is, met op zichzelf staande logica, en scoping alleen binnen PHP-code hoeft te worden uitgevoerd, dan is de kans groot dat Mozart werkt. Je moet het gewoon uitproberen.

PHP-Scoper bekijken, en in paniek wegrennen

Dus ging ik naar PHP-Scoper. Ik heb het echter nooit eens geprobeerd, want ik raakte er onmiddellijk van in paniek.

Om te beginnen ondersteunt deze tool WordPress niet van nature. En om verder te gaan, raden ze aan om een kijkje te nemen in hun eigen Makefile, die er als volgt uitziet:

# 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

En nog 600 regels meer, allemaal zo. Het lijkt op een raadsel. Geloven dat ik die code moest begrijpen alleen om mijn plugin te scopen, deed me zonder omhaal vluchten.

(Welnu, die code begrijpen is hun aanbeveling om de gescopede applicatie te testen, maar het is niet vereist. We kunnen ook gewoon het commando php-scoper add-prefix uitvoeren, het alle magie laten doen, en onze daiquiri gaan drinken.)

Terugkeren naar PHP-Scoper, ditmaal voor goed

Dus, 3 dagen geleden, nam ik een beslissing om scoping te implementeren, hoe dan ook. Ik moest het voor elkaar krijgen.

Ik keerde terug naar PHP-Scoper om het serieus te proberen. Ik wist dat WordPress ermee gescoopt kon worden na het lezen van PHP Scoper: How to Avoid Namespace Issues in your Composer Dependencies (door de briljante mensen van Delicious Brains). Het was gewoon een kwestie van instelling en doorzettingsvermogen.

Ik onderzocht enkele bestaande oplossingen, waaronder:

  • Deze van Lucas Bustamante
  • Deze van Yoast
  • Deze van Google Site Kit
  • Deze van Google Web Stories

Maar ze zagen er allemaal niet volledig bevredigend uit voor mij: de code lijkt ofwel hacky, ofwel fragiel en wachtend om op een gegeven moment te breken.

De Google Web Stories-plugin scopet de code bijvoorbeeld, en draait vervolgens elk conflict terug:

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 );
      // ...
    }
  ]
]

Ik begrijp waarom ze dit doen, maar ik vind het niet fijn. Elke keer dat er naar een nieuwe WordPress-functie wordt verwezen, moeten ze ervoor zorgen dat die ook op deze lijst terechtkomt. Het is te handmatig, te fragiel.

Dit was dus mijn uitdaging: Is er geen eenvoudigere manier om een plugin te scopen, waarbij we vertrouwen op code die we aan onze vrienden en collega's kunnen laten zien zonder te blozen?

PHP-Scoper, de makkelijke manier 😎

Het bleek eigenlijk eenvoudiger dan ik dacht! In slechts een paar uur had ik het allemaal werkend.

Scopen in een paar uur

Wanneer ik "makkelijk" en "uren" zeg, bedoel ik eigenlijk: het werkte allemaal onmiddellijk, maar pas nadat ik 2 maanden had besteed aan het opzetten van de juiste structuur voor de codebase (ik zal dit later beter uitleggen).

Maar het belangrijkste is: als je de juiste opzet voor het project hebt, kan het scopen in no time worden gerealiseerd.

Het probleem met het scopen van WordPress-code is, nou ja, WordPress-code. Het probleem wordt hier uitgelegd, maar het komt erop neer dat alle WordPress-functies en -klassen ook een namespace krijgen. Dus als we WP_Query gebruiken of get_posts aanroepen in onze code, worden deze getransformeerd naar MyPrefixedNamespace\WP_Query en MyPrefixedNamespace\get_posts, wat een epische mislukking veroorzaakt tijdens runtime. En dat kan niet worden vermeden in PHP-Scoper zonder hacks.

Wat is dan de oplossing? Simpel: gebruik WP_Query niet, roep get_posts niet aan, en gebruik geen enkele WordPress-code in de codebase die gescoopt wordt.

Ben ik gek?

Nee, ik ben niet gek, en ik ben er zeker van dat jij het ook niet bent. En ja, ik weet dat we een WordPress-plugin bouwen... Laat me het uitleggen.

Hoe kunnen we WordPress-code vermijden? Door de codebase op te splitsen in 2 sets van packages:

  • Die met WordPress-code bevatten, zonder te verwijzen naar code van enige externe library
  • Die de bedrijfslogica bevatten, zonder enige WordPress-code te bevatten, en alle vereiste afhankelijkheden en verwijzingen naar hun code inclusief

Op deze manier hebben we in plaats van één codebase meerdere codebases (of packages), waarvan sommige gescoopt worden en andere niet, en ze vormen samen de plugin, gekoppeld via Composer.

Vervolgens scopen we het package met WordPress-code niet, waardoor het conflict wordt vermeden. Dit werkt omdat het niet verwijst naar code van een externe afhankelijkheid. Alle verwijzingen zijn intern, zoals MyNamespace\MyPlugin\MyClass. Maar die hoeven niet te worden gescoopt, omdat we veilig kunnen aannemen dat er slechts 1 versie van de plugin op de WordPress-site zal zijn geïnstalleerd, en we onze namespace MyNamespace\* op de whitelist kunnen zetten.

Bovendien, als onze plugin uitbreidbaar is, is het op de whitelist zetten van onze eigen namespace verplicht. Een field resolver voor Gato GraphQL wordt bijvoorbeeld geïmplementeerd door uit te breiden van klasse PoP\ComponentModel\FieldResolvers\AbstractFieldResolver. Als ik die zou scopen, zouden ontwikkelaars gedwongen worden om te verwijzen naar PoP\ComponentModel\FieldResolvers\AbstractFieldResolver voor ontwikkeling, en PrefixedByPoP\PoP\ComponentModel\FieldResolvers\AbstractFieldResolver voor productie. Dat kan niet.

Vervolgens scopen we alleen de bedrijfslogica-packages, die verwijzingen bevatten naar alle externe libraries maar geen WordPress-code.

Samengevat, we wisselen deze strategie:

"Heb één codebase, scoop hem, en draai vervolgens pijnlijk en met veel geduld de schade terug, terwijl je bidt dat geen enkel conflict onopgemerkt blijft en het 💣 ontploft in productie"

Voor deze:

"Splits de codebase op in 2 groepen, scoop alleen de groep die verwijzingen naar de externe afhankelijkheden bevat en geen WordPress-code, en ga genieten van je welverdiende daiquiri 🍹".

Laat me de echte spullen zien

Het is tijd om de worst open te snijden en te zien of er echt vlees in zit 🌭.

4 dagen geleden had ik de volgende code in mijn plugin:

namespace GraphQLAPI\GraphQLAPI\ContentProcessors;
 
use Parsedown;
 
class MarkdownContentParser
{
  protected function getHTMLContent(string $fileContent): string
  {
    return (new Parsedown())->text($markdownContent);
  }
}

Klasse Parsedown komt van de externe afhankelijkheid erusev/parsedown, zoals gedefinieerd in de composer.json van de plugin:

{
  "require": {
    "erusev/parsedown": "^1.7"
  }
}

Mijn plugin bevatte dus verwijzingen naar een externe library, dus moest ik hem scopen om Parsedown te transformeren naar PrefixedByPoP\Parsedown. Maar dit zou ook alle WordPress-code in de plugin scopen, wat de conflicten veroorzaakt.

Dus extraheerde ik de code naar een apart package, genaamd graphql-api/markdown-convertor, en verving de externe afhankelijkheid in composer.json door mijn eigen afhankelijkheid:

{
  "require": {
    "graphql-api/markdown-convertor": "^0.8"
  }
}

Nu vermijdt de plugin de verwijzing naar de externe library; in plaats daarvan verwijst hij naar de service MarkdownConvertorInterface uit het nieuwe package:

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);
    }
}

Verwijzen naar de externe afhankelijkheid wordt gedaan in het nieuwe package:

namespace GraphQLAPI\MarkdownConvertor;
 
use Parsedown;
 
class MarkdownConvertor implements MarkdownConvertorInterface
{
  public function convertMarkdownToHTML(string $markdownContent): string
  {
    return (new Parsedown())->text($markdownContent);
  }
}

Tot slot moeten we:

  • Afhankelijkheid graphql-api/markdown-convertor scopen
  • Het scopen van de plugincode overslaan
  • Namespace GraphQLAPI\* op de whitelist zetten, om te voorkomen dat mijn eigen klassen worden gescoopt

Dit is in grote lijnen de strategie. Vanaf nu is het een herhaling van hetzelfde idee, om alle externe afhankelijkheden uit de code te verwijderen, totdat voilà, de plugin gescoopt kan worden.

De te extraheren afhankelijkheden zijn alleen die uit de require-sectie van je composer.json-bestand; voor require-dev kun je elke afhankelijkheid bewaren, extern of niet, omdat we afhankelijkheden die voor ontwikkeling worden gebruikt niet hoeven te scopen; alleen die welke nodig zijn om de plugin te maken en te leveren, voor productie, moeten worden gescoopt.

Uiteindelijk mag de composer.json van je plugin geen externe afhankelijkheden meer bevatten. Voor mijn plugin ziet het er zo uit:

{
  "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"
  }
}

Al die packages, met namespaces getpop, graphql-api, graphql-by-pop en pop-schema, zijn allemaal van mij: afhankelijkheden die de volledige code voor de plugin bevatten. Ze zijn verdeeld over verschillende namespaces om de code beter te beheren, maar dat hoef je niet te doen: het gebruik van één enkele namespace werkt prima.

Nu, naarmate het aantal packages in je applicatie groeit, moet je ze allemaal in een monorepo hosten, anders word je gek van het maken van pull requests die meer dan één package omvatten (geloof me, ik ben er geweest). In mijn geval zijn al mijn packages gehost in de GatoGraphQL/GatoGraphQL monorepo, en ik houd ze gesynchroniseerd via de geweldige Monorepo Builder (ik moet een artikel schrijven over dit tool, het is een echte levensredder!).

De namespaces voor deze packages zijn PoP, GraphQLAPI, GraphQLByPoP en PoPSchema. Omdat ze van mij zijn, weet ik dat ze slechts één keer in de applicatie voorkomen, en ik kan vermijden ze te scopen.

Om dat te doen, zet ik ze op de whitelist in scoper.inc.php:

return [
  'whitelist' => [
    // Own namespaces
    'PoPSchema\*',
    'PoP\*',
    'GraphQLByPoP\*',
    'GraphQLAPI\*',
    // Own container cache
    'PoPContainer\*',
  ],
];

Het laatste item komt overeen met de dependency injection container, die ook gescoopt moet worden. Standaard krijgt deze container de naam ProjectServiceContainer, direct in de globale namespace. Maar PHP-Scoper ondersteunt geen whitelist van specifieke klassen uit de globale namespace. Daarom heb ik de kunstmatige namespace PoPContainer aan de whitelist toegevoegd, en deze namespace toegewezen bij het opslaan van de container op schijf:

$dumper = new PhpDumper($containerBuilder);
file_put_contents(
  self::$cacheFile,
  $dumper->dump(
    // Save under own namespace to avoid conflicts
    array('namespace' => 'PoPContainer')
  )
);

Je zult opmerken dat, wat de packages betreft, sommige eindigen op -wp (zoals pop-schema/users-wp) terwijl andere dat niet doen (zoals graphql-by-pop/graphql-server). Ja, je hebt het goed geraden: de eerste bevatten WordPress-code en geen verwijzingen naar externe libraries, en de laatste kunnen verwijzingen naar externe libraries bevatten, maar geen WordPress-code wat dan ook.

Vervolgens sla ik het scopen van de WordPress-packages over:

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')
  ]
];

Wat als een WordPress-package moet verwijzen naar een externe library, en dit niet in een ander package kan worden geëxtraheerd? Mijn package getpop/routing-wp is bijvoorbeeld afhankelijk van brain/cortex, en dit is onvermijdelijk.

Ik kan het hele package niet scopen, omdat getpop/routing-wp WordPress-code bevat. In plaats daarvan identificeer ik de bestanden waar die verwijzingen worden gemaakt, en zorg ik ervoor dat ze geen WordPress-code bevatten. Dan kan ik alleen die bestanden scopen.

In dit geval wordt de verwijzing naar Cortex/Brain gemaakt in 2 bestanden, waaronder 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;
        }
      ));
    }
  }
}

Merk je de eigenaardigheid op? Dit is een implementatie van een hook, maar er wordt geen add_action aangeroepen, omdat ik hier geen WordPress-code mag hebben. In plaats daarvan roept het de functie addAction aan van service HooksAPIInterface, en deze service is geïmplementeerd door klasse HooksAPI in package getpop/hooks-wp, waar we WordPress-code kunnen hebben:

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);
  }
}

Nu de code netjes is opgesplitst, kunnen we die 2 bestanden die verwijzen naar externe afhankelijkheden scopen:

return [
  'finders' => [
    Finder::create()->append([
      'vendor/getpop/routing-wp/src/Component.php',
      'vendor/getpop/routing-wp/src/Hooks/SetupCortexHookSet.php',
    ])
  ]
];

Eerder vermeldde ik dat het instellen van de scoping een paar uur duurde, maar pas na 2 maanden werk. Welnu, dit voorbeeld toont wat ik bedoelde: het echte werk zit in het netjes opdelen van de codebase in de 2 sets.

In mijn geval duurde het werk 2 maanden omdat het detailniveau extreem was: de plugin werd een samenstelling van 125 packages! Maar dit is een uitzonderlijk geval, met als doel de onderliggende server van de plugin CMS-agnostisch te maken, zodat een implementatie voor andere CMSs/frameworks ondersteund kan worden door simpelweg de overeenkomstige -wp-packages opnieuw te implementeren.

(Ik heb over deze strategie in detail geschreven in het artikel Abstracting WordPress Code To Reuse With Other CMSs: Concepts en Implementation.)

Het is zeker behoorlijk wat werk, maar de verbeterde netheid van de code maakt het de moeite waard. En niet alleen voor het scopen van de plugin, wat voor mij een totale verrassing was, en ik ben er nog steeds opgetogen over mijn onverwachte geluk. Ik run PHPStan en PHPUnit bijvoorbeeld afzonderlijk op WordPress- en niet-WordPress-code, wat me veel hoofdpijn bespaart.

Zodra de codebase is opgeruimd, wordt de wereld plotseling een veel betere plek.

Testen

Hoe testen we dit beest dan?

De oplossing die ik bedacht heb, is vertrouwen op Rector, hetzelfde tool dat ik gebruik voor het downgraden van code van PHP 7.4, voor ontwikkeling, naar 7.1, voor productie.

Het idee is als volgt:

  1. Scoop de plugin
  2. Analyseer hem met Rector, waarbij je een willekeurige regel toepast (het maakt niet uit welke)

Als er iets mis ging tijdens het scopen, kan Rector een klasse niet laden en zal het een fout gooien. Als klasse Brain\Cortex bijvoorbeeld gescoopt werd als PrefixedByPoP\Brain\Cortex, maar een verwijzing ernaar als Brain\Cortex bleef staan, zal het autoloaden van deze klasse mislukken.

Dit is mijn GitHub Action voor testen (working-directory wordt gebruikt, omdat ik vanuit de root van de monorepo werk, maar het scopen gebeurt in de pluginmap):

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: .
 

En dit is mijn Rector-configuratie:

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/*',
  ]);
};

Je zult opmerken dat sommige afhankelijkheidsbestanden, zoals erusev/parsedown/Parsedown.php', moeten worden toegevoegd aan Option::AUTOLOAD_PATHS. Dat komt omdat het scopen van de composer.json van het package niet 100% betrouwbaar is, en dan kan hun autoloading mislukken.

Wanneer dat gebeurt, zal Rector klagen dat een bepaalde klasse niet kon worden geladen. Van daaruit identificeren we het overeenkomstige bestand en voegen het handmatig toe aan de autoloading-paden.

Bekijk de resultaten

Dit is de broncode van de plugin, en dit is de gescopede (en naar PHP 7.1 gedowngrade) versie.

Zoek de 7 verschillen 😁. (Ik geef je een hint: zoek naar PrefixedByPoP.)

En dit is het uiteindelijke graphql-api.zip pluginbestand, klaar om op je site te installeren.

Dat is alles. Ik hoop dat dit nuttig is geweest 😃💪🚀


Abonneer je op onze nieuwsbrief

Blijf op de hoogte van alle updates over Gato GraphQL.