Direi che è arrivato il momento di concludere il nostro piccolo viaggio introduttivo alla scoperta di come creare un’API GraphQL con Laravel. Nell’articolo precedente, pubblicato un paio di settimane, fa, avevo spiegato le basi di Lighthouse e di come usarlo per creare, facilmente, un’API GraphQL usando il nostro framework preferito.
L’ultima volta ci siamo lasciati dopo aver visto, un po’ più da vicino, le prime query sulla nostra entità Movie.
Oggi vedremo più da vicino come funzionano le mutation, come vengono gestite le relazioni tra entità e, infine, qualche esempio su altri tool che Lighthouse ci mette a disposizione per facilitarci il lavoro. Pronti? Partiamo dalle…
Mutation!
Come già spiegato nell’articolo precedente, nel mondo di GraphQL una Query è un’operazione di lettura, mentre una Mutation riguarda, appunto, una “mutazione” dei nostri dati. Può essere un inserimento, una modifica ma anche una cancellazione.
Per avere a disposizione queste operazioni nella nostra API, per fortuna, non dobbiamo fare molto. Andiamo a modificare il nostro file dello schema, aggiungendo
1 2 3 4 5 6 7 8 9 |
// ... type Mutation { addMovie(title: String!, year: Int!): Movie! @create updateMovie(id: ID!, title: String, year: Int) : Movie @update deleteMovie(id: ID!) : Movie @delete } // ... |
Come si può intuire, abbiamo definito tre mutation, una per ogni operazione da effettuare.
- addMovie, che si occupa dell’aggiunta di un nuovo film all’API;
- updateMovie, che ci consente di modificare un film già esistente;
- deleteMovie, necessario a cancellare un film esistente;
La sintassi è abbastanza lineare:
- definisco il nome della mutation;
- i parametri sono quelli di cui avrò bisogno nel momento in cui userò la mutation corrispondente (titolo ed anno in fase di creazione, con aggiunta dell’id in fase di modifica; per concludere, solo l’id in fase di cancellazione);
- in ogni caso, mi verrà ritornata l’entità Movie corrispondente;
- le direttive finali, @create, @update e @delete spiegano a Lighthouse quale operazione stiamo chiedendo. Sotto il cofano, il package si occuperà del resto, permettendoci di implementare le tre operazioni senza scrivere codice aggiuntivo;
Facciamo una prova? Apriamo il playground visto nell’articolo precedente (dovrebbe essere raggiungibile su localhost/graphql-playground, ed eseguiamo una mutation come
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
mutation { addMovie(title: "La Grande Fuga", year: 1963) { id title year } } // il cui risultato sarà... { "data": { "addMovie": { "id": "1", "title": "La Grande Fuga", "year": 1963 } } } |
Se vediamo qualcosa del genere, tutto è andato a buon fine. Come esercizio, provate ad eseguire le altre due mutation! 😉
Relazioni tra entità
Abbiamo visto le operazioni più basilari con una singola entità. Come gestire, però, le relazioni tra diverse entità? Proviamo ad immaginarci una nuova entità: il director, il regista. Un’entità di cui intendo salvare tre informazioni:
- nome;
- cognome;
- link alla pagina wikipedia;
Ovviamente, non scordiamoci la chiave esterna per la relazione di cui avremo bisogno, nella tabella movies. Ecco la migration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?php use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; use Illuminate\Support\Facades\Schema; class CreateDirectorsTable extends Migration { /** * Run the migrations. * * @return void */ public function up() { Schema::create('directors', function (Blueprint $table) { $table->id(); $table->string('first_name'); $table->string('last_name'); $table->string('wikipedia_page'); $table->timestamps(); }); Schema::table('movies', function (Blueprint $table) { $table->unsignedBigInteger('director_id')->nullable(); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::table('movies', function (Blueprint $table) { $table->dropColumn('director_id'); }); Schema::dropIfExists('directors'); } } |
Non dimentichiamoci, ovviamente, di creare il model e definire la relazione in esso.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Director extends Model { use HasFactory; public function movies() { return $this->hasMany(Movie::class); } } |
Fatto questo, andiamo ad aggiungere un po’ di cose nel nostro schema:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
// Aggiungiamo il type... type Director { id: ID! first_name: String! last_name: String! wikipedia_page: String! created_at: DateTime! updated_at: DateTime! movies: [Movie!]! @hasMany(type: CONNECTION) } // Aggiungiamo il campo "director" al film... type Movie { // ... director: Director! @belongsTo } // Definiamo le mutation nell'apposito blocco... type Mutation { // Modifichiamo quelle relative ai film addMovie(title: String!, year: Int!, director_id: Int!): Movie! @create updateMovie(id: ID!, title: String, year: Int, director_id: Int!) : Movie @update // Aggiungiamo quelle relative al regista addDirector(first_name: String!, last_name: String!, wikipedia_page: String!): Director! @create updateDirector(first_name: String!, last_name: String!, wikipedia_page: String!): Director! @create deleteDirector(first_name: String!, last_name: String!, wikipedia_page: String!): Director! @create } |
Come probabilmente avete intuito, le due direttive @belongsTo e @hasMany descrivono i due “lati” della relazione tra film e regista. Esattamente come abbiamo fatto nel model Eloquent.
Come visto per le mutation di base, poco fa, queste due direttive sono specifiche di Lighthouse e ci permettono di usare le relazioni tra i nostri model senza dover scrivere altro codice. Ancora una volta, stesso approccio di prima: si usa quasi esclusivamente il file di schema, con l’aiuto delle direttive, per ottenere il risultato di cui abbiamo bisogno.
Torniamo al playground ed aggiungiamo un primo director con cui fare qualche prova.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
// Eseguiamo mutation { addDirector(first_name: "Paolo", last_name: "Sorrentino", wikipedia_page: "https://it.wikipedia.org/wiki/Paolo_Sorrentino") { id first_name last_name wikipedia_page } } // il cui risultato sarà... { "data": { "addDirector": { "id": 1, "first_name": "Paolo", "last_name": "Sorrentino", "wikipedia_page": "https://it.wikipedia.org/wiki/Paolo_Sorrentino" } } } // Infine, eseguiamo mutation { addMovie(title: "La Grande Bellezza", year: 2013, director_id: 1) { id title year } } // per aggiungere un film con, associato, l'id del regista. La risposta... { "data": { "addMovie": { "id": "1", "title": "La Grande Bellezza", "year": 2013 } } } |
Fatto questo, abbiamo dei dati su cui poter effettuare una query.
Perché non provare ad effettuare la query per ottenere un film specifico, ma stavolta chiedendo anche le informazioni sul regista? Così:
1 2 3 4 5 6 7 8 9 10 11 12 |
query { movie(id: 1){ id, title, year, director { first_name, last_name, wikipedia_page } } } |
Con una singola query, stiamo chiedendo:
- i dettagli del film;
- i dettagli del regista;
La risposta, stavolta, sarà…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
{ "data": { "movie": { "id": "1", "title": "La Grande Bellezza", "year": 2013, "director": { "first_name": "Paolo", "last_name": "Sorrentino", "wikipedia_page": "https://it.wikipedia.org/wiki/Paolo_Sorrentino" } } } } |
Nota: ho preparato questa query usando tutti i campi delle entità, ma nulla mi vieta di escludere uno o più campi (sia in Movie che in Director) per adattare la query al mio caso d’uso. Fate una prova voi stessi: eliminate dalla query qualche campo del Movie e del Director, e guardate cosa ne esce fuori!
L’aspetto più interessante di questa caratterestica di GraphQL è l’assoluta flessibilità di ogni query, una volta che questa viene ben costruita. Lavorando in questo modo, diventa molto più semplice coprire casi d’uso diversi senza dover andare a scrivere altro (e magari replicare parti di) codice.
Altre pillole di Lighthouse
Lighthouse, ovviamente, non si ferma alle sole operazioni di base. Coprirlo tutto in poche pagine sarebbe impossibile, ma prima di lasciarci volevo farvi dare un’occhiata a qualche altra direttiva che ho trovato degna di nota.
Validazione dei dati
So bene cosa avete pensato prima, mentre scrivevo i metodi di creazione/modifica di film e registi.
“E la validazione? Possiamo mai mandare i dati così, a caso?”
Ovviamente no. Come spiegato molto bene nell’apposita pagina della documentazione, tramite un comando è possibile creare una classe “validator” apposita per il caso d’uso che vogliamo coprire. A quel punto, una volta specificate le regole nel formato che già conosciamo, non serve altro che definire il validatore usando la direttiva @validator
1 2 3 4 5 6 7 8 |
input UpdateUserInput @validator { id: ID name: String } type Mutation { updateUser(id: ID!, name: String): User @validator } |
Ah, per i casi più semplici comunque esiste anche la direttiva @rules… 😉
Throttling
Le API sono una soluzione meravigliosa, anche e soprattutto quando si ha a che fare con partner/utenti esterni che possono consumarle per il loro business. Tuttavia, bisogna sempre tenere sotto controllo tutto quello che riguarda la sicurezza e l’eventuale abuso di un’API.
Lighthouse ci mette a disposizione la direttiva @throttle come prima difesa verso gli “utilizzatori compulsivi” della nostra API.
Molto interessante la possibilità di definire facilmente un numero di tentativi in un certo lasso di tempo, e la durata di tale lasso di tempo.
Ordinamento
Abbiamo visto la semplicità con cui è possibile implementare, per le nostre query, la paginazione. La cosa che serve sempre, subito dopo la paginazione, è l’ordinamento crescente/decrescente partendo da un campo specifico.
Anche qui, indovinate? C’è una direttiva: l’utilissima @orderBy che ci consente di definire per quali campi sarà possibile ordinare i risultati. Sarà possibile, quindi, creare una query come questa:
1 2 3 4 5 |
{ posts(orderBy: [{ column: POSTED_AT, order: ASC }]) { title } } |
Et voilà!
Concludendo
Anche oggi ho sforato il buon proposito di tenermi “breve”. Fa niente: l’importante è che l’articolo vi sia stato, in qualche modo, utile a capirne qualcosa di più. GraphQL non è qualcosa che si adatta a tutti i progetti, ma per determinati casi d’uso può essere davvero di grande aiuto e fare la differenza.
Voi ci avete mai messo le mani? Fatemi sapere… sono curioso!
Se volete tenervi aggiornati su quello che scrivo, iscrivetevi alla newsletter! Trovate qui il form di iscrizione, oppure potete dare un’occhiata qui a lato. Alla prossima!