Blog

🦸🏻‍♂️ Presentazione: Headless WordPress senza WordPress

Leonardo Losoviz
Di Leonardo Losoviz ·

Fin dal fiasco di Matt Mullenweg contro WPEngine, ho notato sempre più persone su Reddit (e altrove) che chiedono alternative a WordPress, non necessariamente per abbandonare WordPress (almeno non immediatamente), ma per capire quali opzioni hanno e quanto sarebbe dolorosa un'eventuale migrazione. Vogliono sapere come cautelarsi.

Per chi lavora con headless WordPress, Gato GraphQL offre ora una nuova interessante funzionalità: Headless WordPress senza WordPress.

Questo articolo spiega tutto a riguardo, descrivendo come ciò sia possibile e mostrando un video dimostrativo.

Eseguire Gato GraphQL come applicazione PHP autonoma

Gato GraphQL è stato costruito utilizzando componenti PHP autonomi, gestiti tramite Composer, in modo tale che tutti i componenti PHP che costituiscono il server GraphQL non dipendano da WordPress!

Di conseguenza, il server GraphQL può essere eseguito come applicazione PHP autonoma, e puoi includerlo in qualsiasi applicazione PHP, basata su WordPress o su qualcos'altro.

Se per un determinato caso d'uso la tua applicazione non ha bisogno di accedere ai dati di WordPress, allora, almeno per quel caso d'uso, sei pronto.

Questo video dimostra un caso d'uso di questo tipo: interagire con l'API di GitHub, per scaricare/installare artefatti da GitHub Actions durante lo sviluppo:

Demo Headless WordPress senza WordPress: esecuzione di una query GraphQL

Nel video, la query GraphQL esegue una richiesta HTTP per recuperare gli ultimi plugin di Gato GraphQL generati in GitHub Actions, che vengono caricati come artefatti al merge di una pull request.

Gli URL degli artefatti presenti nella risposta GraphQL vengono poi iniettati in WP-CLI, in modo che i plugin vengano installati automaticamente su un server web DEV locale, per eseguire i test.

(Spiegherò più nel dettaglio nell'ultima sezione di questo articolo.)

In questo caso d'uso, dato che non viene acceduto alcun dato di WordPress, il server GraphQL può già essere eseguito come applicazione PHP autonoma.

Se ne avessi bisogno, potrei persino utilizzarlo all'interno del mio workflow GitHub Actions!

Migrare un'applicazione headless WordPress

Ogni volta che accedi effettivamente ai dati di WordPress, vediamo come eseguire tutto ciò senza WordPress.

Lo schema GraphQL fornito da Gato GraphQL contiene campi per recuperare i dati di WordPress: posts, users, comments, tags, categories, ecc.

Il codice nei resolver PHP che recupera i dati di WordPress dipende da WordPress; quel codice non può essere eseguito su un'applicazione non-WordPress.

Tuttavia, Gato GraphQL ha ciascuno di questi resolver implementato tramite 2 package:

  1. Uno PHP "vanilla", che contiene tutto il codice generico
  2. Uno specifico per WordPress, che contiene le invocazioni effettive ai metodi di WordPress che soddisfano quel resolver

Ad esempio, in questa query GraphQL:

{
  posts {
    id
    title
  }
}

...la logica per recuperare i post è composta da:

  1. Il campo Root.posts: vive nel package posts generico
  2. La sua risoluzione per WordPress tramite il metodo get_posts: vive nel package posts-wp specifico per WordPress.

La suddivisione del codice tra i package non-WordPress/WordPress è di circa l'80/20%, il che significa che l'80% del codice è riutilizzabile con un altro framework/CMS, e solo il 20% del codice dovrebbe essere reimplementato.

Inoltre, tutte le funzionalità di Gato GraphQL sono distribuite tramite moduli, e i moduli possono essere abilitati/disabilitati a piacimento.

Moduli dello schema
Moduli dello schema

Modules è una funzionalità implementata a fini di sicurezza: se non hai bisogno di esporre i dati degli utenti nella tua API pubblica, puoi disabilitare il modulo Users, e i campi corrispondenti (come Root.users) non verranno mai aggiunti allo schema.

I moduli sono direttamente associati ai package PHP sottostanti. Di conseguenza, quando si esegue Gato GraphQL come applicazione autonoma, possiamo caricare selettivamente i moduli/package di cui abbiamo bisogno, e nessun altro.

Ad esempio, se la tua applicazione mostra solo dati per post, categorie e tag, allora devono essere caricati solo i package posts-wp, categories-wp e tags-wp (insieme alle loro dipendenze).

Poi, durante la migrazione da WordPress (diciamo, verso Laravel o Symfony), solo quei 3 package specifici per WordPress dovrebbero essere reimplementati per il nuovo framework/CMS, e nient'altro.

Di conseguenza, puoi utilizzare headless WordPress oggi, sapendo che in futuro potrai migrare la tua applicazione verso un altro framework o CMS con il minimo sforzo.

Passare a Gato GraphQL da un'altra API

Se stai già facendo headless WordPress, è probabile che la tua applicazione utilizzi la WP REST API o WPGraphQL.

Sfortunatamente, con una qualsiasi di queste due API sei vincolato a WordPress: non esiste una WP REST API al di fuori di WordPress, e WPGraphQL non può essere eseguito senza WordPress.

Fortunatamente, è possibile sostituire una di esse con Gato GraphQL, e ottenere la capacità di migrare la tua applicazione headless WordPress al di fuori di WordPress.

Questi 2 passaggi sarebbero quindi necessari:

  1. Passare da WP REST API o WPGraphQL a Gato GraphQL
  2. Reimplementare i package specifici per WordPress richiesti

Vediamo come si può effettuare la transizione dell'API.

Da WP REST API alle persisted queries di Gato GraphQL

Con l'estensione Persisted Queries puoi pubblicare endpoint simili a REST, composti utilizzando GraphQL.

Per ciascuno degli endpoint REST della tua applicazione, puoi creare un endpoint di persisted query corrispondente che recupera gli stessi dati, e utilizzare quell'endpoint al suo posto.

Ad esempio, la seguente query GraphQL può sostituire 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
  }
}

Grazie alla gerarchia di API, la persisted query può essere pubblicata sotto il percorso /graphql-query/wp/v2/posts/, semplificando così il mapping degli endpoint.

Per replicare l'endpoint REST /wp-json/wp/v2/posts/{id}/, che recupera i dati del post con l'ID indicato, possiamo fornire l'ID del post tramite il parametro URL postId.

Ad esempio, la seguente persisted query può essere invocata tramite 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
  }
}

Da WPGraphQL a Gato GraphQL

Lo schema GraphQL di WPGraphQL e quello di Gato GraphQL sono simili ma leggermente diversi, quindi devono essere adattati.

Lo starter WordPress con Next.js leoloso/next-wordpress-starter funziona sia con WPGraphQL che con Gato GraphQL. Lo starter utilizza la stessa logica JS per entrambi i server, solo le queries GraphQL sono diverse.

Questo starter fornisce diversi esempi di adattamento delle queries tra i due server. Ad esempio, questa query WPGraphQL:

fragment PostFields on Post {
  id
  categories {
    edges {
      node {
        databaseId
        id
        name
        slug
      }
    }
  }
  databaseId
  date
  isSticky
  postId
  slug
  title
}

...viene adattata in questo modo per 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
}

In dettaglio: eseguire Gato GraphQL come applicazione PHP autonoma

Ecco la spiegazione dettagliata del video dimostrativo presentato in precedenza.

Forniamo la query GraphQL da eseguire nel file retrieve-github-artifacts.gql.

La query si connette all'API di GitHub ottenendo il token di accesso dalla variabile d'ambiente GITHUB_ACCESS_TOKEN. Genera dinamicamente il percorso completo per l'endpoint actions/artifacts a partire dalle variabili fornite, e poi invia una richiesta HTTP verso di esso.

Dalla risposta, estrae poi l'"URL di download" all'interno di ciascun elemento artefatto, e invia richieste HTTP asincrone verso di essi. Dall'header Location di ciascuno di questi "URL di download" otteniamo l'URL effettivo del file scaricabile.

Infine, stampa tutti gli URL insieme separati da uno spazio, per facilitarne l'iniezione in 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 logica PHP carica direttamente il codice dal plugin Gato GraphQL e dal bundle "Power Extensions" (necessario per inviare richieste HTTP e per altre funzionalità).

Come applicazione PHP autonoma, dobbiamo indicare esplicitamente quali moduli vengono inizializzati, e fornire qualsiasi configurazione non predefinita.

Ad esempio, indichiamo al modulo SendHTTPRequests di consentire la connessione a https://api.github.com/repos, e al modulo EnvironmentFields di consentire l'accesso alla variabile d'ambiente GITHUB_ACCESS_TOKEN.

Nota che lo schema GraphQL viene generato la prima volta che la query GraphQL viene eseguita, e messo in cache su disco. In questo modo, dalla 2ª volta in poi, nessun codice per calcolare lo schema viene eseguito, rendendo l'esecuzione più veloce.

Infine, l'applicazione autonoma inizializza il server GraphQL, esegue la query su di esso, e stampa la risposta.

<?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();

Per eseguire la query GraphQL, lanciamo nel terminale (utilizzando jq per formattare in modo leggibile l'output JSON):

php retrieve-github-artifacts.php | jq

Infine, per estrarre gli URL degli artefatti dalla risposta GraphQL, e iniettarli in WP-CLI, eseguiamo:

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 --activate

Come mostra il video, siamo in grado di eseguire Gato GraphQL senza WordPress.


Iscriviti alla nostra newsletter

Resta aggiornato su tutte le novità di Gato GraphQL.