🕸 Come e dove GraphQL può migliorare WordPress, completando la REST API
Aggiornamento 01/05/2024: Dai un'occhiata al confronto Gato GraphQL vs WP REST API.
Lo scorso weekend ho pubblicato l'articolo del blog 🦸🏿♂️ Gato GraphQL ora viene transpilato da PHP 8.0 a 7.1.
Dopo aver condiviso il post su Reddit's /r/php, la community ha avviato una vivace discussione su quanto valga la pena usare GraphQL in WordPress, su quanto sia diverso dalla WP REST API, e su quanto sia giustificato portare l'ennesima API in WordPress.
Penso che la maggior parte dei commenti siano azzeccati, mentre altri tralasciano alcune informazioni chiave. GraphQL non è solo un'interfaccia, ma anche un'implementazione. Questo significa che diversi server GraphQL, di diversi fornitori, possono essere stati progettati per dare priorità a caratteristiche differenti. In quanto tale, non possiamo sempre avere un'aspettativa unificata di ciò che GraphQL offre, o una comprensione completa di come funziona un motore GraphQL.
Ad esempio, l'esperienza GraphQL in WordPress e in Laravel sarà diversa, così come l'esperienza fornita dai diversi server, WPGraphQL o Gato GraphQL.
Questo articolo è il mio punto di vista sulla questione, in risposta a diversi dei commenti del post su Reddit.
GraphQL vs WP REST API
[È un'idea così pessima] avere un'API GraphQL sopra WordPress che usa già la propria REST API. Usa semplicemente la REST API. [Source]
Sia la REST API che GraphQL servono allo stesso scopo: fornire all'applicazione i dati di cui ha bisogno. Tuttavia, si comportano diversamente nel modo in cui ci riescono: mentre REST ha endpoint predefiniti che forniscono un insieme specifico di dati, GraphQL può fornire esattamente i dati necessari.
Questo comportamento diverso può avere un impatto diretto sulle prestazioni dell'applicazione. Con REST, se abbiamo bisogno di recuperare un elenco di articoli più alcuni dati di ogni autore dell'articolo, ciò richiederà l'invio di richieste aggiuntive. Forse 1 richiesta aggiuntiva per tutti i dati degli autori, oppure 1 richiesta aggiuntiva per autore. Nel frattempo, il visitatore del sito web potrebbe attendere che la pagina venga renderizzata.
GraphQL migliora questa situazione, poiché possiamo recuperare direttamente tutti i dati degli articoli e degli autori in un'unica richiesta, e il rendering della pagina web sarà più veloce:
{
posts {
id
title
excerpt
date
url
author {
id
name
url
}
}
}Quindi, anche se abbiamo già la REST API in WordPress, ciò non significa che sia sempre lo strumento più adatto per ogni compito. Certo, possiamo sempre usarla, ma se abbiamo anche accesso a GraphQL, allora possiamo decidere di usare questa API ogni volta che offre un vantaggio rispetto a REST, e ne usciremo avvantaggiati.
Configurazione iniziale difficile per GraphQL + Dover scrivere i resolver
C'è sicuramente un argomento a favore del fatto che la configurazione iniziale per GraphQL è esponenzialmente più alta rispetto a REST; hai ragione sul fatto che le associazioni devono essere configurate. [Source]
E...
Quello che tu e quasi tutti gli altri sul web tralasciate è che affinché questo formato di API funzioni, dovete scrivere il parser (resolver + tipi) che porta con sé una serie di problemi che non sono presenti con REST. [Source]
Questi commenti non sono completamente accurati, perché sia WPGraphQL che Gato GraphQL hanno già mappato il modello di dati di WordPress nello schema GraphQL (WPGraphQL completamente, il mio plugin per la maggior parte).
Poi, dopo aver installato uno di questi plugin, puoi iniziare immediatamente a recuperare dati per la tua applicazione, senza bisogno di creare alcun resolver, o di configurare associazioni tra entità.
È vero che, per recuperare dati personalizzati dalle entità proprie dell'applicazione (come dai CPT), questi devono essere mappati tramite resolver, e dovrai farlo. Ma questo non è diverso da REST: se hai bisogno di dati personalizzati dal tuo CPT, dovrai creare un endpoint REST per recuperare quei dati personalizzati. Anche un endpoint personalizzato è un resolver.
Quindi, per quanto riguarda la necessità di resolver, REST e l'API GraphQL sono praticamente identici.
Ora, navigando tra siti web e documentazione, dà l'impressione che GraphQL richieda più sforzo per la configurazione. Quindi c'è del vero in questa presunzione.
Credo che ci siano alcune ragioni per questo. Innanzitutto, GraphQL coinvolge (almeno) due parti:
- il concetto di cosa sia, e come funziona
- i server che forniscono un'implementazione concreta
Quando si naviga nella documentazione di GraphQL, come il sito ufficiale graphql.org, essa si concentra sui concetti dietro GraphQL, entrando nel dettaglio dei resolver, cosa sono e perché sono necessari.
Questo è utile quando si costruisce un'applicazione da zero, come ad esempio usando Laravel e Lighthouse. In quel caso, hai bisogno di codificare i tuoi resolver (ma allo stesso modo avresti bisogno di creare i tuoi endpoint REST).
Tuttavia, WordPress è già l'applicazione, e WPGraphQL e Gato GraphQL sono soluzioni. Questi due plugin hanno già creato i resolver per noi, quindi non dobbiamo preoccuparcene (analogamente a come anche la WP REST API fornisce un insieme iniziale di endpoint, quindi non dobbiamo preoccuparcene).
Inoltre, GraphQL è più orientato agli sviluppatori, e la sua documentazione sembra parlare direttamente agli sviluppatori. Gli sviluppatori creano i resolver lato server, e gli sviluppatori consumano quei resolver con queries personalizzate lato client. Poiché costruire i resolver è un compito per sviluppatori, esso compare naturalmente e spesso.
Per REST, l'aspettativa (credo) è che l'endpoint che fornisce i dati richiesti esista già (come fornito dalla WP REST API). Se non esiste, solo allora dobbiamo preoccuparci di configurare un endpoint personalizzato. Quindi, c'è meno enfasi sulla creazione di resolver per REST.
Quindi, sia REST che GraphQL forniscono i dati richiesti. Ma mentre REST incoraggia un approccio statico, in cui gli endpoint dovrebbero già esistere, e solo quando non esistono ce ne preoccupiamo, GraphQL incoraggia un approccio dinamico, in cui ogni query è fatta su misura, e poi possiamo codificare il resolver perfetto per essa.
Quindi, in conclusione, non ci sono differenze fondamentali tra REST e GraphQL, solo interpretazioni diverse su come debbano soddisfare i loro requisiti.
Vulnerabilità + Considerazioni di sicurezza in GraphQL
Un giorno vedremo un'enorme vulnerabilità di GraphQL, perché scrivere interpreti sicuri è davvero difficile. [Source]
E...
WordPress è già così massiccio che ha già un enorme bersaglio sulla schiena; aggiungere QUALSIASI plugin aggiunge molto rischio, e un plugin che si offre di esporre letteralmente tutto WordPress, inclusi molti esempi di codice per aggirare il modello di sicurezza, è un grosso no per me. L'output non guidato dal tema dovrebbe essere il più ristretto possibile (inesistente a meno che non lo richieda io) oltre a ciò che è assolutamente necessario esporre. Spero che questo non finisca mai nel core. [Source]
GraphQL impone effettivamente rischi di sicurezza aggiuntivi che dobbiamo affrontare. Sono pienamente d'accordo con questa sensazione.
Ma non penso che sia un problema così bloccante, al punto da impedire una potenziale inclusione di GraphQL nel core di WP. Inoltre, non penso nemmeno che sia davvero difficile da risolvere.
Ciò che serve è che il server GraphQL sfrutti i meccanismi di sicurezza esistenti di WordPress, e poi che lo sviluppatore usi questi meccanismi, assicurandosi che un determinato campo possa essere accessibile solo dagli utenti appropriati:
- l'utente è loggato?
- l'utente è l'amministratore?
- l'utente ha un determinato ruolo o capacità?
- l'utente è l'autore dell'articolo?
Per soddisfare questa proposta, Gato GraphQL offre delle Liste di controllo degli accessi, così possiamo definire chi può accedere a ciascun campo e direttiva, e tramite configurazione.
Ora, a volte usare una sola ACL non basta, e il server GraphQL deve fornire misure di sicurezza aggiuntive. Descriverò ciò su cui sto lavorando in questo momento per l'imminente v0.8 di Gato GraphQL.
Il campo posts (per recuperare i dati degli articoli) non richiede autorizzazione, qualsiasi utente può accedervi, sia loggato che non. Quindi, per ragioni di sicurezza, recupera solo gli articoli pubblicati.
Ma ci sono situazioni in cui abbiamo bisogno di recuperare anche articoli in bozza/in attesa/cestinati, come:
- Per costruire un sito web statico, che viene eseguito dall'amministratore, con accesso a tutti i dati del sito
- Per gli autori dell'articolo, per elencare tutte le bozze così da poterle continuare a modificare
Allora ho elaborato il seguente schema. Per recuperare gli articoli, ci saranno 3 campi:
posts: aperto a chiunque, può recuperare solo gli articoli pubblicatimyPosts: aperto a chiunque, recupera solo gli articoli dell'utente loggato, con qualsiasi stato (pubblicato/bozza/in attesa/cestinato)postsForAdmin: solo l'amministratore può accedervi, recupera qualsiasi articolo con qualsiasi stato
E poi, postsForAdmin è disabilitato per impostazione predefinita, quindi non compare nemmeno nello schema GraphQL, a meno che l'amministratore non lo abiliti esplicitamente (e, molto probabilmente, sarà abilitato solo per costruire siti statici).
Un'altra situazione è quando un determinato campo può recuperare sia dati pubblici che privati. Ad esempio, il campo option recupera dati dalla tabella wp_options. Alcune voci sono pubbliche (come blogname), mentre altre non lo sono (come admin_email).
Una situazione simile riguarda il recupero dei valori meta, attraverso i campi Post.metaValue, User.metaValue, e altri. Ad esempio, i meta dell'utente includono la voce wp_capabilities, che è certamente privata, mentre description è pubblica. E poi c'è last_name, che può essere pubblico o privato a seconda dell'applicazione.
Per rendere sicuro l'accesso a questi dati, il plugin permetterà di specificare quali voci possono essere interrogate tramite una lista di autorizzazione/rifiuto nella pagina delle impostazioni, accettando sia la voce completa che una regex:

Poi, interrogare l'opzione consentita funzionerà, mentre l'opzione rifiutata restituirà semplicemente null:
{
# This option is allowed
siteName: optionValue(name: "blogname")
# This optionValue is not allowed
adminEmail: optionValue(name: "admin_email")
}Con misure di sicurezza appropriate fornite dal server GraphQL, e il buon senso dello sviluppatore, creare un'API GraphQL sicura non dovrebbe essere difficile.
GraphQL che mette in ginocchio il DB
GraphQL è una sintassi ricca che permette di esprimere queries relazionali profonde, quindi per un ecosistema come WordPress, dove l'estensibilità del modello di dati deriva dal pattern entity-attribute-value, questo si traduce in incredibili quantità di usura su un database, che può rendere il tuo sito non reattivo se la query GraphQL è profonda, complicata o ricorsiva. WordPress è già famoso per la sua capacità di mettere in ginocchio un'istanza MySQL/MariaDB, quindi aggiungere GraphQL potrebbe peggiorare di molto le cose se le queries non sono scritte, autenticate e limitate in frequenza correttamente. [Source]
Mettere in ginocchio il DB è una preoccupazione seria per i server GraphQL. Descriverò come Gato GraphQL tenta di evitare questo scenario.
Gato GraphQL evita che il problema N+1 si verifichi, già per progettazione architetturale. Ci riesce facendo sì che sia il motore a essere responsabile del caricamento delle entità dal database, non lo sviluppatore.
Quando si risolvono connessioni in un resolver, il valore restituito è l'ID (o la lista di ID) dell'oggetto/degli oggetti, e non l'oggetto stesso. Ad esempio, recuperare l'autore del custom post si fa così:
class CustomPostFieldResolver extends AbstractDBDataFieldResolver
{
private CustomPostUserTypeAPIInterface $customPostUserTypeAPI;
public function getClassesToAttachTo(): array
{
return [
CustomPostFieldInterfaceResolver::class,
];
}
public function getSchemaFieldType(string $fieldName): ?string
{
return match($fieldName) {
'author' => SchemaDefinition::TYPE_ID,
default => null,
};
}
public function resolveValue(
TypeResolverInterface $typeResolver,
object $customPost,
string $fieldName,
array $fieldArgs = []
): mixed {
switch ($fieldName) {
case 'author':
return $this->customPostUserTypeAPI->getAuthorID($customPost);
}
return null;
}
public function resolveFieldTypeResolverClass(
TypeResolverInterface $typeResolver,
string $fieldName
): ?string {
switch ($fieldName) {
case 'author':
return UserTypeResolver::class;
}
return null;
}
}Disponendo dell'ID dell'entità del DB da resolveValue, e del tipo dell'oggetto da resolveFieldTypeResolverClass (rappresentato tramite la classe UserTypeResolver), il motore GraphQL può quindi caricare i dati per l'oggetto.
Per caricare i dati, il motore usa un algoritmo super efficiente: ha complessità temporale O(n), dove n è il numero di tipi nella query, non il numero di nodi.
L'algoritmo raggiunge questa efficienza perché non attraversa un grafo, ma converte la struttura dei dati in uno stack di componenti, che è molto più semplice da risolvere. (Il "graph" in GraphQL è un concetto, non un'implementazione concreta.)
Quindi, anche se la query ha più livelli, ognuno dei quali recupera molte entità, l'algoritmo riesce comunque a reggerla piuttosto bene. Ad esempio, non c'è un grosso impatto nell'eseguire la seguente query, che ha una profondità di 10 livelli:
{
posts(pagination: { limit: 10 }) {
excerpt
title
url
author {
name
url
posts(pagination: { limit: 10 }) {
title
tags(pagination: { limit: 10 }) {
slug
url
posts(pagination: { limit: 10 }) {
title
comments(pagination: { limit: 10 }) {
content
date
author {
name
posts(pagination: { limit: 10 }) {
title
url
comments(pagination: { limit: 10 }) {
content
date
author {
name
username
url
}
}
}
}
}
}
}
}
}
}
}L'eccezione a questa efficienza è quando si recuperano valori meta, attraverso Post.metaValue, User.metaValue, Comment.metaValue, PostTag.metaValue e PostCategory.metaValue (e anche il loro campo metaValues). Questo perché le funzioni di WordPress (get_post_meta, get_user_meta, ecc) recuperano dati per 1 ID alla volta, il che significa che ogni entità richiederà una chiamata al database per recuperare il suo valore meta. Di conseguenza, la risoluzione dei valori meta scala in base al numero di nodi, non al numero di tipi (il commento dell'OP coglie nel segno, a questo riguardo).
Per evitare che malintenzionati usino e abusino dei campi meta, Gato GraphQL (in v0.8) verrà distribuito con questi campi disabilitati per impostazione predefinita. Poi, l'amministratore deve abilitarli esplicitamente e, nel farlo, può collocare questi campi sotto una qualche Lista di controllo degli accessi, così che in nessun momento il DB sia a rischio di attacco.
Anche il rate limiting è un'ottima idea, ho in programma di supportarlo per qualche prossima release.
E poi c'è l'analisi e l'imposizione di limiti sulla complessità della query (come quanti livelli di profondità abbia). Il server GraphQL risolve la query con complessità temporale O(n), quindi non c'è molto danno che possa essere fatto per quanto riguarda i loop. Tuttavia, una singola query potrebbe comunque recuperare quantità illimitate di dati dal DB, e questo è qualcosa che potremmo voler evitare.
Ad esempio, questa semplice query porterà un'enorme quantità di dati in un'unica richiesta (il mio sito demo ha a malapena qualche centinaio di record, quindi posso permettermi di dimostrare l'esecuzione della query):
{
posts000: posts(pagination: { limit: 100 }) {
...PostFields
}
posts100: posts(pagination: { limit: 100, offset: 100 }) {
...PostFields
}
posts200: posts(pagination: { limit: 100, offset: 200 }) {
...PostFields
}
posts300: posts(pagination: { limit: 100, offset: 300 }) {
...PostFields
}
posts400: posts(pagination: { limit: 100, offset: 400 }) {
...PostFields
}
posts500: posts(pagination: { limit: 100, offset: 500 }) {
...PostFields
}
posts600: posts(pagination: { limit: 100, offset: 600 }) {
...PostFields
}
posts700: posts(pagination: { limit: 100, offset: 700 }) {
...PostFields
}
posts800: posts(pagination: { limit: 100, offset: 800 }) {
...PostFields
}
posts900: posts(pagination: { limit: 100, offset: 900 }) {
...PostFields
}
}
fragment PostFields on Post {
id
title
content
date
}Come si può notare, la query non ha nemmeno bisogno di essere annidata per creare problemi. Quindi analizzare la complessità di una query è una faccenda delicata, che richiederà una messa a punto fine per essere utile.
Spero di supportare anche l'analisi delle queries, ma non è nella mia lista di priorità elevate, perché con una combinazione delle altre funzionalità (come le persisted queries o i custom endpoints, abbinati alle Liste di controllo degli accessi) possiamo già tenere lontani i malintenzionati, e noi stessi non dovremmo (non dovremmo!) abusare del nostro stesso servizio GraphQL.