Blog

👶🏻 Ringiovanire WordPress grazie a GraphQL

Leonardo Losoviz
Di Leonardo Losoviz ·

WordPress è un CMS datato: essendo stato inventato più di 17 anni fa, è pieno di codice PHP che, con una nuova possibilità, verrebbe scritto in modo diverso.

GraphQL è un'interfaccia moderna per accedere ai dati. Si noti bene la parola "interfaccia": non le importa come sia implementato il sistema di dati sottostante, ma soltanto come esporre i dati.

Cosa succede quando mettiamo insieme questi due elementi? Come dovremmo progettare l'interfaccia GraphQL per accedere ai dati di WordPress?

Ci sono un paio di strategie ovvie che possiamo adottare:

  1. Rispettare la tradizione e fornire un mapping che mantenga il modello di dati di WordPress così com'è, incluso il debito tecnico accumulato negli anni

  2. Correggere il debito tecnico, fornendo un'interfaccia che esponga i dati in modo astratto, non necessariamente legato a WordPress

Entrambi gli approcci hanno vantaggi e svantaggi, e non c'è giusto o sbagliato. È semplicemente una questione di scelte, di dare priorità a un comportamento rispetto a un altro.

Per il plugin Gato GraphQL ho scelto il secondo approccio, tentando di creare uno schema GraphQL che, pur essendo basato su WordPress e funzionando per WordPress, non sia legato a WordPress (per esempio, rimuovendo nomi e relazioni incoerenti).

Il risultato è che GraphQL ringiovanisce WordPress: pur avendo ancora WordPress come nostro CMS sottostante, con il suo codice PHP datato, il suo livello dati può essere ricreato da zero, basato sul buon senso e non sulla tradizione. Il livello dati torna dall'essere un adolescente a diventare di nuovo un bambino piccolo.

GraphQL + WordPress rock

Il risultato è uno schema GraphQL che rappresenta il modello di dati di WordPress, supportando anche le mutation nidificate.

Vediamo come è stato realizzato.

Il modello di dati di WordPress

WordPress possiede le seguenti entità:

  • post
  • pagine
  • custom post
  • elementi multimediali
  • utenti
  • ruoli utente
  • tag
  • categorie
  • commenti
  • blocchi
  • proprietà meta
  • altre (opzioni, plugin, temi, ecc.)

Queste entità possono avere una gerarchia. Per esempio, post, pagina ed elementi multimediali sono tutti custom post type, e tag e categorie sono entrambi tassonomie.

Ecco il diagramma del database di WordPress, che mostra come vengono memorizzati i dati di tutte le entità:

Il diagramma del database di WordPress

Il mapping è una replica esatta del diagramma del DB?

Nel mappare il database di WordPress in uno schema GraphQL, lo stesso diagramma sopra viene rispettato 1 a 1?

No, non lo è. Sebbene il diagramma del database sia un'implementazione reale, GraphQL è un'interfaccia per accedere ai dati dal client. Questi due sono correlati, ma possono essere diversi. A GraphQL non importa del database: non ragiona in comandi SQL, né sa che esistono tabelle del database chiamate wp_posts e wp_users.

Quindi non abbiamo bisogno di preoccuparci troppo del diagramma del database quando creiamo lo schema GraphQL per WordPress. Ciò significa che possiamo produrre uno schema GraphQL che corregge parte del debito tecnico del modello di dati di WordPress.

Mappare il modello di dati di WordPress come schema GraphQL

Eseguiamo il mapping. Per prima cosa, mappiamo le entità originali come tipi, per quanto possibile. Dall'elenco delle entità nel modello di dati di WordPress, produciamo i seguenti tipi per lo schema GraphQL:

  • Post
  • Page
  • Media
  • User
  • UserRole
  • PostTag
  • PostCategory
  • Comment

Poi, aggiungiamo tutti i campi attesi a ogni tipo. Per rappresentare lo schema, possiamo usare l'SDL, ovvero lo Schema Definition Language. (Questo viene usato solo a scopo di documentazione; il plugin stesso non usa l'SDL per codificare lo schema: è tutto codice PHP).

Ecco i campi (tra molti altri) per un Post:

type Post {
  id: ID!
  title: String
  content: String
  excerpt: String
  publishedAt: Date!
}

Ecco i campi (tra molti altri) per uno User:

type User {
  id: ID!
  name: String
  email: String!
}

Creiamo anche le connessioni corrispondenti, che sono campi che restituiscono un'altra entità (invece di uno scalare, come un numero o una stringa). Per esempio, rappresentiamo un post che ha un autore, e un utente che possiede dei post:

type Post {
  author: User!
}
 
type User {
  posts: [Post]
}

I campi e le connessioni possono anche accettare argomenti. Per esempio, permettiamo che Post.date venga formattato, e che User.posts ricerchi voci e ne limiti il numero:

type Post {
  date(format: String): Date!
}
 
type User {
  posts(limit: Int, search: String): [Post]
}

Continuiamo a farlo per tutte le entità del modello di dati di WordPress. Una volta terminato, arriveremo allo schema GraphQL per WordPress, visibile usando il client Voyager (disponibile come "Interactive Schema" nel menu del plugin):

Lo schema GraphQL per WordPress

Questo schema presenta delle somiglianze con il diagramma del database di WordPress, ma anche molte differenze. Analizziamole.

Le operazioni senza entità vengono mappate come campi Root

Il diagramma del database di WordPress rappresenta come vengono memorizzati i dati, quindi non c'è un "inizio". GraphQL, invece, è un'interfaccia per recuperare dati, perciò deve esserci uno stadio iniziale da cui eseguire la query.

Questo stadio iniziale è il tipo Root, o, per essere più precisi, i tipi QueryRoot e MutationRoot (per gestire rispettivamente le query e le mutation).

In questi due tipi, mappiamo tutte le operazioni che non dipendono da un'entità, come quando si esegue get_posts(), get_users() o wp_signon():

type QueryRoot {
  posts: [Post]!
  users: [User]!
}
 
type MutationRoot {
  logUserIn(username: String, password: String): User
}

I campi non hanno bisogno di avere lo stesso nome o la stessa firma dell'operazione che rappresentano. Per esempio, chiamare il campo logUserIn può essere considerato più appropriato di signOn.

Tutte le mutation vanno sotto MutationRoot

Ci sono operazioni che dipendono da un'entità, come wp_update_post(), che viene applicata su un determinato post. La mutation corrispondente nello schema GraphQL deve essere aggiunta al tipo MutationRoot, perché è così che funziona GraphQL.

Questa operazione viene quindi mappata così:

type MutationRoot {
  updatePost(input: {
    postID: ID!,
    newTitle: String,
    newContent: String
  }): Post
}

Questo plugin supporta anche le mutation nidificate, che sono offerte come funzionalità opt-in (perché questo non è un comportamento GraphQL standard). Le mutation possono quindi essere aggiunte anche sotto qualsiasi tipo, non solo MutationRoot. In questo caso, otteniamo:

type Post {
  update(input: {
    newTitle: String,
    newContent: String
  }): Post!
}

Gestire i custom post

In GraphQL non esiste l'ereditarietà dei tipi. Di conseguenza, non possiamo avere un tipo CustomPost e dichiarare che Post e Page lo estendono.

GraphQL offre due risorse per compensare questa mancanza: le interfacce e i tipi union.

Per la prima, creiamo un'interfaccia CustomPost per lo schema, dichiarando tutti i campi attesi da un custom post, e definiamo i tipi Post e Page affinché implementino l'interfaccia:

interface CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Post implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}
 
type Page implements CustomPost {
  title: String
  content: String
  excerpt: String
  date(format: String): Date!
}

Per la seconda, creiamo un tipo CustomPostUnion per lo schema che restituisce tutti i custom post type:

union CustomPostUnion = Post | Page

E facciamo in modo che i campi restituiscano questo tipo quando opportuno:

type QueryRoot {
  customPost(id: ID): CustomPostUnion
  customPosts: [CustomPostUnion]!
}
 
type User {
  customPosts: [CustomPostUnion]
}
 
type Comment {
  customPost: CustomPostUnion!
}

Come si può notare, nello schema GraphQL dobbiamo affermare esplicitamente quando trattiamo dei post e quando dei custom post, dato che non sono la stessa cosa! Chiamare questi due in modo intercambiabile è un debito tecnico di WordPress, che possiamo correggere.

Per questo motivo, un custom post viene sempre chiamato CustomPost e non Post, un campo che tratta i custom post viene sempre chiamato customPosts e non posts, e un argomento di campo che riceve l'ID per un custom post viene chiamato customPostID e non postID (anche se è così che si chiama nella funzione WordPress mappata).

L'aspettativa è quindi sempre chiara:

  • il campo User.customPosts può restituire un elenco di qualsiasi custom post, inclusi post e pagine, mentre User.posts restituisce solo post
  • il campo Root.setFeaturedImageOnCustomPost può aggiungere un'immagine in evidenza a qualsiasi custom post, ed è per questo che non si chiama setFeaturedImageOnPost

Non raggruppare i tag (e le categorie) sotto un unico tipo

Perché il tipo PostTag (e lo stesso vale per PostCategory) si chiama così, anziché semplicemente Tag?

Perché, eseguendo questa query (dove un prodotto è un CPT), i risultati del campo tags per i post e per i prodotti saranno sempre diversi, senza sovrapposizioni:

query {
  posts {
    tags {
      id
      name
    }
  }
  products {
    tags {
      id
      name
    }
  }
}

I tag aggiunti ai post non compariranno quando si recuperano i tag per i prodotti, e viceversa (a meno che un prodotto non usi anche la tassonomia post_tag, ma allora può essere rappresentato anche con il tipo PostTag). Questo non rappresenta un grosso problema in WordPress, dato che questi elementi possono essere considerati righe diverse della stessa tabella del database. Ma ha importanza per GraphQL, che è fortemente tipizzato.

È quindi una buona decisione di progettazione mantenere queste entità separate, sotto i propri tipi, e fare in modo che i tag per i post vengano restituiti sotto il tipo PostTag e, se un plugin personalizzato implementa il proprio CPT di prodotto, deve usare il tipo ProductTag per i suoi tag.

Dare agli elementi multimediali la propria identità

Le entità multimediali in WordPress sono custom post type, solo perché era comodo dal punto di vista dell'implementazione. Tuttavia, lo schema GraphQL può evitare questo debito tecnico, e modellare gli elementi multimediali come un'entità distinta, non come custom post.

Ciò comporta le seguenti decisioni per lo schema GraphQL:

  • Quando si interroga il campo customPosts, esso non recupererà gli elementi multimediali
  • Il tipo Media non implementa l'interfaccia CustomPost, e non farà parte del tipo CustomPostUnion
  • Il tipo Media non ha molti dei campi attesi da un custom post type, come excerpt, date e status. Ha invece solo i campi attesi da un elemento multimediale:
type Media {
  id: ID!
  src: String!
  width: Int
  height: Int
}

Identificare e mappare gli enum

In alcune situazioni, WordPress usa valori fissi da un dato insieme. Per esempio, lo stato di un post può essere solo "publish", "draft", "pending" o "trash".

In GraphQL, possiamo trattarli come enum (invece che come stringhe), e creare un tipo di enumerazione corrispondente. Seguendo lo standard GraphQL, gli enum dovrebbero essere scritti in maiuscolo, così:

enum CUSTOM_POST_STATUS {
  PUBLISH
  DRAFT
  PENDING
  TRASH
}

Tuttavia, in tal caso la query non può essere usata direttamente per interagire con WordPress, dato che eseguire get_posts( [ "post_status" => "PUBLISH" ] ) non funziona.

Quindi, come compromesso, manteniamo questi valori enum in minuscolo:

enum CUSTOM_POST_STATUS {
  publish
  draft
  pending
  trash
}

Mappare tipi aggiuntivi

I blocchi non sono direttamente visibili nel diagramma del database di WordPress, dato che vengono memorizzati in wp_posts (non esiste una tabella wp_blocks), ma costituiscono comunque un'entità distinta.

Introduciamo quindi il tipo Block per mapparli:

type Post {
  blocks: [Block]
}
 
type Block {
  type: String!
  attributes: JSONObject
}

Iscriviti alla nostra newsletter

Resta aggiornato su tutte le novità di Gato GraphQL.