đŠžđ»ââïž PrĂ©sentation : Headless WordPress sans WordPress
Depuis le fiasco Matt Mullenweg contre WPEngine, j'ai remarqué de plus en plus de personnes sur Reddit (et ailleurs) demandant des alternatives à WordPress, pas nécessairement pour quitter WordPress (du moins pas immédiatement), mais pour comprendre quelles options elles ont, et à quel point une migration potentielle serait douloureuse. Elles veulent savoir comment se prémunir.
Pour les personnes qui travaillent avec headless WordPress, Gato GraphQL offre désormais une nouvelle fonctionnalité intéressante : Headless WordPress sans WordPress.
Cet article explique tout à ce sujet, en décrivant comment c'est possible et en montrant une vidéo de démonstration.
Exécuter Gato GraphQL comme une application PHP autonome
Gato GraphQL a été construit en utilisant des composants PHP autonomes, gérés via Composer, de telle maniÚre que tous les composants PHP constituant le serveur GraphQL ne dépendent pas de WordPress !
Ainsi, le serveur GraphQL peut s'exécuter en tant qu'application PHP autonome, et vous pouvez l'inclure dans n'importe quelle application PHP, basée sur WordPress ou autre chose.
Si pour un cas d'usage votre application n'a pas besoin d'accĂ©der aux donnĂ©es WordPress, alors, au moins pour ce cas d'usage, vous ĂȘtes prĂȘt.
Cette vidéo démontre un tel cas d'usage : Interagir avec l'API GitHub, pour télécharger/installer des artefacts depuis GitHub Actions pendant le développement :
Dans la vidĂ©o, la requĂȘte GraphQL exĂ©cute une requĂȘte HTTP pour rĂ©cupĂ©rer les derniers plugins Gato GraphQL gĂ©nĂ©rĂ©s dans GitHub Actions, qui sont tĂ©lĂ©versĂ©s comme artefacts lors du merge d'une pull request.
Les URLs des artefacts de la réponse GraphQL sont ensuite injectées dans WP-CLI, afin que les plugins soient automatiquement installés sur un serveur local DEV, pour exécuter des tests.
(J'expliquerai plus en détail dans la derniÚre section de cet article.)
Dans ce cas d'usage, comme aucune donnée WordPress n'est accédée du tout, le serveur GraphQL peut déjà s'exécuter comme une application PHP autonome.
Si j'en avais besoin, je pourrais mĂȘme l'utiliser dans mon workflow GitHub Actions !
Migrer une application headless WordPress
Lorsque vous accédez effectivement aux données WordPress, voyons comment exécuter cela sans WordPress.
Le schéma GraphQL fourni par Gato GraphQL contient des champs pour récupérer des données WordPress : posts, users, comments, tags, categories, etc.
Le code dans les résolveurs PHP qui récupÚre les données WordPress dépend de WordPress ; ce code ne peut pas s'exécuter sur une application non-WordPress.
Cependant, Gato GraphQL a chacun de ces résolveurs implémentés via 2 packages :
- Un package PHP « vanilla », contenant tout le code générique
- Un package spécifique à WordPress, contenant les invocations réelles aux méthodes WordPress qui satisfont ce résolveur
Par exemple, dans cette requĂȘte GraphQL :
{
posts {
id
title
}
}...la logique pour récupérer les articles est composée de :
- Le champ
Root.posts: Il vit dans le packagepostsgénérique - Sa résolution pour WordPress via la méthode
get_posts: Il vit dans le packageposts-wpspécifique à WordPress.
La rĂ©partition du code entre les packages non-WordPress/WordPress est d'environ 80/20 %, ce qui signifie que 80 % du code est rĂ©utilisable avec un autre framework/CMS, et seulement 20 % du code devrait ĂȘtre rĂ©implĂ©mentĂ©.
De plus, toute la fonctionnalitĂ© de Gato GraphQL est distribuĂ©e via des modules, et les modules peuvent ĂȘtre activĂ©s/dĂ©sactivĂ©s Ă volontĂ©.

Modules est une fonctionnalité implémentée à des fins de sécurité : Si vous n'avez pas besoin d'exposer des données utilisateur dans votre API publique, vous pouvez désactiver le module Users, et les champs correspondants (comme Root.users) ne seront jamais ajoutés au schéma.
Les modules sont directement associés aux packages PHP sous-jacents. Ainsi, lors de l'exécution de Gato GraphQL comme application autonome, nous pouvons charger sélectivement les modules/packages dont nous avons besoin, et aucun autre.
Par exemple, si votre application n'affiche que des donnĂ©es pour les posts, les catĂ©gories et les tags, alors seuls les packages posts-wp, categories-wp et tags-wp (ainsi que leurs dĂ©pendances) doivent ĂȘtre chargĂ©s.
Ensuite, lors de la migration depuis WordPress (disons, vers Laravel, ou Symfony), seuls ces 3 packages spĂ©cifiques Ă WordPress devraient ĂȘtre rĂ©implĂ©mentĂ©s pour le nouveau framework/CMS, et rien d'autre.
En conséquence, vous pouvez utiliser headless WordPress aujourd'hui, en sachant que vous pourrez plus tard migrer votre application vers un autre framework ou CMS avec un effort minimal.
Passer Ă Gato GraphQL depuis une autre API
Si vous faites déjà du headless WordPress, il y a de fortes chances que votre application utilise soit la WP REST API, soit WPGraphQL.
Malheureusement, avec l'une ou l'autre de ces deux APIs, vous ĂȘtes liĂ© Ă WordPress : Il n'y a pas de WP REST API en dehors de WordPress, et WPGraphQL ne peut pas s'exĂ©cuter sans WordPress.
Heureusement, il est possible de remplacer l'une ou l'autre par Gato GraphQL, et d'acquérir la capacité de migrer votre application headless WordPress en dehors de WordPress.
Ces 2 étapes seraient alors nécessaires :
- Passer de WP REST API ou WPGraphQL Ă Gato GraphQL
- Réimplémenter les packages spécifiques à WordPress requis
Voyons comment la transition d'API peut ĂȘtre effectuĂ©e.
WP REST API vers les persisted queries de Gato GraphQL
Avec l'extension Persisted Queries, vous pouvez publier des endpoints similaires à REST, composés en utilisant GraphQL.
Pour chacun des endpoints REST dans votre application, vous pouvez crĂ©er un endpoint de persisted query correspondant qui rĂ©cupĂšre les mĂȘmes donnĂ©es, et utiliser cet endpoint Ă la place.
Par exemple, la requĂȘte GraphQL suivante peut remplacer l'endpoint REST /wp-json/wp/v2/posts/ :
{
posts {
id
date: dateStr(format: "Y-m-d\\TH:i:s")
modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
slug
status
link: url
title: self {
rendered: title
}
content: self {
rendered: content
},
excerpt: self {
rendered: excerpt
}
author
featured_media: featuredImage
sticky: isSticky
categories
tags
}
}GrĂące Ă la hiĂ©rarchie d'API, la persisted query peut ĂȘtre publiĂ©e sous le chemin /graphql-query/wp/v2/posts/, facilitant ainsi le mappage des endpoints.
Pour répliquer l'endpoint REST /wp-json/wp/v2/posts/{id}/, qui récupÚre les données pour l'article avec l'ID donné, nous pouvons fournir l'ID de l'article sous le paramÚtre URL postId.
Par exemple, la persisted query suivante peut ĂȘtre invoquĂ©e sous l'endpoint /graphql-query/wp/v2/posts/single/?postId={id} :
query GetPost($postId: ID!) {
post(by: { id: $postId }) {
id
date: dateStr(format: "Y-m-d\\TH:i:s")
modified: modifiedDateStr(format: "Y-m-d\\TH:i:s")
slug
status
link: url
title: self {
rendered: title
}
content: self {
rendered: content
},
excerpt: self {
rendered: excerpt
}
author
featured_media: featuredImage
sticky: isSticky
categories
tags
}
}WPGraphQL vers Gato GraphQL
Le schĂ©ma GraphQL de WPGraphQL et de Gato GraphQL sont similaires mais lĂ©gĂšrement diffĂ©rents, ils doivent donc ĂȘtre adaptĂ©s.
Le starter WordPress avec Next.js leoloso/next-wordpress-starter fonctionne avec WPGraphQL ou Gato GraphQL. Le starter utilise la mĂȘme logique JS pour l'un ou l'autre serveur, seules les requĂȘtes GraphQL sont diffĂ©rentes.
Ce starter fournit plusieurs exemples d'adaptation des requĂȘtes entre les deux serveurs. Par exemple, cette requĂȘte WPGraphQL :
fragment PostFields on Post {
id
categories {
edges {
node {
databaseId
id
name
slug
}
}
}
databaseId
date
isSticky
postId
slug
title
}...est adaptée comme ceci pour Gato GraphQL :
fragment PostFields on Post {
id
categories: self {
edges: categories(pagination: { limit: -1 }) {
node: self {
databaseId: id
id
name
slug
}
}
}
databaseId: id
date: dateStr
isSticky
postId: id
slug
title
}En détail : Exécuter Gato GraphQL comme une application PHP autonome
Voici l'explication détaillée de la vidéo de démonstration présentée plus tÎt.
Nous fournissons la requĂȘte GraphQL Ă exĂ©cuter dans le fichier retrieve-github-artifacts.gql.
La requĂȘte se connecte Ă l'API GitHub en obtenant le token d'accĂšs depuis la variable d'environnement GITHUB_ACCESS_TOKEN. Elle gĂ©nĂšre dynamiquement le chemin complet pour l'endpoint actions/artifacts Ă partir des variables fournies, puis envoie une requĂȘte HTTP contre celui-ci.
Depuis la rĂ©ponse, elle extrait ensuite l'« URL de tĂ©lĂ©chargement » dans chaque Ă©lĂ©ment d'artefact, et envoie des requĂȘtes HTTP asynchrones contre elles. Depuis l'en-tĂȘte Location de chacune de ces « URLs de tĂ©lĂ©chargement », nous obtenons l'URL rĂ©elle du fichier tĂ©lĂ©chargeable.
Enfin, elle affiche toutes les URLs ensemble séparées par un espace, pour faciliter leur injection dans WP-CLI.
# File retrieve-github-artifacts.gql
query RetrieveProxyArtifactDownloadURLs(
$repoOwner: String!
$repoProject: String!
$perPage: Int = 1
$artifactName: String = ""
) {
githubAccessToken: _env(name: "GITHUB_ACCESS_TOKEN")
@remove
# Create the authorization header to send to GitHub
authorizationHeader: _sprintf(
string: "Bearer %s"
values: [$__githubAccessToken]
)
@remove
# Create the authorization header to send to GitHub
githubRequestHeaders: _echo(
value: [
{ name: "Accept", value: "application/vnd.github+json" }
{ name: "Authorization", value: $__authorizationHeader }
]
)
@remove
@export(as: "githubRequestHeaders")
githubAPIEndpoint: _sprintf(
string: "https://api.github.com/repos/%s/%s/actions/artifacts?per_page=%s&name=%s"
values: [$repoOwner, $repoProject, $perPage, $artifactName]
)
# Use the field from "Send HTTP Request Fields" to connect to GitHub
gitHubArtifactData: _sendJSONObjectItemHTTPRequest(
input: {
url: $__githubAPIEndpoint
options: { headers: $__githubRequestHeaders }
}
)
@remove
# Finally just extract the URL from within each "artifacts" item
gitHubProxyArtifactDownloadURLs: _objectProperty(
object: $__gitHubArtifactData
by: { key: "artifacts" }
)
@underEachArrayItem(passValueOnwardsAs: "artifactItem")
@applyField(
name: "_objectProperty"
arguments: { object: $artifactItem, by: { key: "archive_download_url" } }
setResultInResponse: true
)
@export(as: "gitHubProxyArtifactDownloadURLs")
}
query CreateHTTPRequestInputs
@depends(on: "RetrieveProxyArtifactDownloadURLs")
{
httpRequestInputs: _echo(value: $gitHubProxyArtifactDownloadURLs)
@underEachArrayItem(passValueOnwardsAs: "url")
@applyField(
name: "_objectAddEntry"
arguments: {
object: {
options: { headers: $githubRequestHeaders, allowRedirects: null }
}
key: "url"
value: $url
}
setResultInResponse: true
)
@export(as: "httpRequestInputs")
@remove
}
query RetrieveActualArtifactDownloadURLs
@depends(on: "CreateHTTPRequestInputs")
{
_sendHTTPRequests(inputs: $httpRequestInputs) {
artifactDownloadURL: header(name: "Location")
@export(as: "artifactDownloadURLs", type: LIST)
}
}
query PrintSpaceSeparatedArtifactDownloadURLs
@depends(on: "RetrieveActualArtifactDownloadURLs")
{
spaceSeparatedArtifactDownloadURLs: _arrayJoin(
array: $artifactDownloadURLs
separator: " "
)
}La logique PHP charge directement le code depuis le plugin Gato GraphQL, et depuis le bundle « Power Extensions » (nĂ©cessaire pour envoyer des requĂȘtes HTTP, et d'autres fonctionnalitĂ©s).
En tant qu'application PHP autonome, nous devons indiquer explicitement quels modules sont initialisés, et fournir toute configuration non-par-défaut.
Par exemple, nous indiquons au module SendHTTPRequests de permettre la connexion Ă https://api.github.com/repos, et au module EnvironmentFields de permettre l'accĂšs Ă la variable d'environnement GITHUB_ACCESS_TOKEN.
Notez que le schĂ©ma GraphQL est gĂ©nĂ©rĂ© la premiĂšre fois que la requĂȘte GraphQL est exĂ©cutĂ©e, et mis en cache sur disque. Ainsi, Ă partir de la 2Ăšme fois, aucun code pour calculer le schĂ©ma n'est exĂ©cutĂ©, rendant l'exĂ©cution plus rapide.
Enfin, l'application autonome initialise le serveur GraphQL, exĂ©cute la requĂȘte contre celui-ci, et affiche la rĂ©ponse.
<?php
// File retrieve-github-artifacts.php
declare(strict_types=1);
use GraphQLByPoP\GraphQLServer\Server\StandaloneGraphQLServer;
use PoP\Root\Container\ContainerCacheConfiguration;
// Load the GraphQL server via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql/vendor/scoper-autoload.php');
// Load the PRO extensions via the standalone PHP components
require_once (__DIR__ . '/wordpress/wp-content/plugins/gatographql-power-extensions-bundle/vendor/scoper-autoload.php');
// Modules required in the GraphQL query
$moduleClasses = [
\PoPSchema\EnvironmentFields\Module::class,
\PoPSchema\FunctionFields\Module::class,
\GraphQLByPoP\ExportDirective\Module::class,
\GraphQLByPoP\DependsOnOperationsDirective\Module::class,
\GraphQLByPoP\RemoveDirective\Module::class,
\PoPSchema\ApplyFieldDirective\Module::class,
\PoPSchema\SendHTTPRequests\Module::class,
\PoPSchema\ConditionalMetaDirectives\Module::class,
\PoPSchema\DataIterationMetaDirectives\Module::class,
];
// Configure the modules
$moduleClassConfiguration = [
\PoP\GraphQLParser\Module::class => [
\PoP\GraphQLParser\Environment::ENABLE_MULTIPLE_QUERY_EXECUTION => true,
\PoP\GraphQLParser\Environment::USE_LAST_OPERATION_IN_DOCUMENT_FOR_MULTIPLE_QUERY_EXECUTION_WHEN_OPERATION_NAME_NOT_PROVIDED => true,
\PoP\GraphQLParser\Environment::ENABLE_RESOLVED_FIELD_VARIABLE_REFERENCES => true,
\PoP\GraphQLParser\Environment::ENABLE_COMPOSABLE_DIRECTIVES => true,
],
\PoPSchema\SendHTTPRequests\Module::class => [
\PoPSchema\SendHTTPRequests\Environment::SEND_HTTP_REQUEST_URL_ENTRIES => [
'#https://api.github.com/repos/(.*)#',
],
],
\PoPSchema\EnvironmentFields\Module::class => [
\PoPSchema\EnvironmentFields\Environment::ENVIRONMENT_VARIABLE_OR_PHP_CONSTANT_ENTRIES => [
'GITHUB_ACCESS_TOKEN',
],
],
];
// Cache the schema to disk, to speed-up execution from the 2nd time onwards
$containerCacheConfiguration = new ContainerCacheConfiguration('MyGraphQLServer', true, 'retrieve-github-artifacts', __DIR__ . '/tmp');
// Initialize the server
$graphQLServer = new StandaloneGraphQLServer($moduleClasses, $moduleClassConfiguration, [], [], $containerCacheConfiguration);
/**
* GraphQL query to execute, stored in its own .gql file
*
* @var string
*/
$query = file_get_contents(__DIR__ . '/retrieve-github-artifacts.gql');
// GraphQL variables
$variables = [
'repoOwner' => 'GatoGraphQL',
'repoProject' => 'GatoGraphQL',
'perPage' => 3
];
// Execute the query
$response = $graphQLServer->execute(
$query,
$variables,
);
// Print the response
echo $response->getContent();Pour exĂ©cuter la requĂȘte GraphQL, nous lançons dans le terminal (en utilisant jq pour afficher joliment la sortie JSON) :
php retrieve-github-artifacts.php | jqEnfin, pour extraire les URLs des artefacts depuis la réponse GraphQL, et les injecter dans WP-CLI, nous exécutons :
GITHUB_ARTIFACT_URLS=$(php retrieve-github-artifacts.php \
| grep -E -o '"spaceSeparatedArtifactDownloadURLs\":"(.*)"' \
| cut -d':' -f2- | cut -d'"' -f2- | rev | cut -d'"' -f2- | rev \
| sed 's/\\\//\//g')
wp plugin install ${GITHUB_ARTIFACT_URLS} --force --activateComme le montre la vidéo, nous sommes capables d'exécuter Gato GraphQL sans WordPress.