Creare API GraphQL con il Package Laravel-GraphQL

Con un'API GraphQL è possibile fornire al frontend della nostra applicazione esattamente quello di cui ha bisogno. Come crearne una con Laravel?
francesco
Francesco Malatesta
11/01/2017 in Package, Tutorial

Mai sentito parlare di GraphQL? No?

Nessun problema: si tratta di una cosa abbastanza recente, tutto sommato. L'ultimo aggiornamento della specifica, che puoi trovare qui, risale a Ottobre 2016.

GraphCosa?

In poche parole, GraphQL è un linguaggio costruito appositamente per definire delle richieste da mandare ad un'API. Non si tratta di un linguaggio di programmazione, ma di un insieme di regole che definiscono come queste richieste devono essere costruite.

La cosa interessante, di tutta questa faccenda, è che una richiesta GraphQL non specifica soltanto "cosa" chiedere, ma anche "come" chiederlo. Lo so, detta così non è il massimo della chiarezza. Facciamo un esempio, riprendendo quello della specifica.

{
  user(id: 4) {
    name
  }
}

supponiamo di inviare una richiesta GET, ad un'API GraphQL che abbiamo costruito, con il body appena visto. Il risultato dell'operazione sarà...

{
  "user": {
    "name": "Mark Zuckerberg"
  }
}

Ok, ma cosa significa? Un passo alla volta.

Innanzitutto, il segmento user(id: 4) specifica che vogliamo i dati di un utente. Più precisamente, quello il cui valore del campo id è 4. Dopo questa dichiarazione, si apre un blocco in cui appare un solo elemento: name. Questo elemento è la proprietà che stiamo chiedendo all'API, e che vogliamo ci venga restituita.

In poche parole, stiamo chiedendo alla nostra API di restituirci il nome dell'utente 4: "Mark Zuckerberg" è il risultato, all'interno di un oggetto "user" opportunamente creato.

Certo, vista così la cosa è riduttiva. Tuttavia, GraphQL è fondato su principi ben specifici:

  • Gerarchia: GraphQL ha come obiettivo il dare l'importanza alla gerarchia degli elementi. Questo perchè oggi, effettivamente, lo sviluppo di un prodotto prevede la costruzione di view "legate" tra loro da specifiche relazioni, in modo tale da favorirne modularità e riutilizzo. Come paradigma, quindi, GraphQL vuole facilitare il più possibile questo approccio;
  • Incentrato sul Prodotto: essendo molto legato a ciò che il frontend di un'applicazione può richiedere, GraphQL concettualmente è molto legato alla visione del prodotto che si vuole costruire;
  • Tipizzato: lavorare con le query GraphQL significa definire ogni singola query che si vuole usare a livello applicativo. Si tratta di darle una caratterizzazione ben specifica, e questo giova alla qualità del codice, visto che solo le query che hanno una definizione verranno riconosciute come valide;
  • Query Specifiche per il Client: una volta definita la query, le possibilità d'uso sono molteplici e la flessibilità tanta. Una delle caratteristiche più interessanti e peculiari di GraphQL consiste nel poter scegliere cosa il server deve farci ritornarci o no;
  • Introspettività: un altro aspetto interessante di GraphQL. Un server GraphQL non deve solo permettermi di effettuare una certa query, ma anche di sapere quali query posso effettuare oppure no. Un gran bel vantaggio per i client!

Se è la prima volta che ci avviciniamo a GraphQL, probabilmente avremo capito molto poco di questo elenco. Non è un problema, comunque, perchè capiremo meglio a breve. Prima di proseguire, però, vediamo dal vivo un esempio che dovrebbe risultarci quanto meno familiare: le API di Facebook. Andando sulla pagina del Graph API Explorer possiamo infatti fare qualsiasi richiesta vogliamo ai server di Facebook, e controllarne i risultati.

Di default l'interfaccia dovrebbe mostrare la richiesta me?fields=id,name. Clicchiamo su "Submit" per vedere il risultato. Se abbiamo un account Facebook dovremmo poter vedere il nostro id ed il nostro nome.

Nel mio caso:

{
  "id": "10203905429385894",
  "name": "Francesco Malatesta"
}

Proviamo adesso a cambiare richiesta, aggiungendo il campo gender alla lista di fields richiesti, modificando la stringa della nostra query in me?fields=id,name,gender. Inviamo la richiesta e, sempre nel mio caso...

{
  "id": "10203905429385894",
  "name": "Francesco Malatesta",
  "gender": "male"
}

Se ci facciamo caso, l'endpoint in realtà è lo stesso. Quello che è cambiato è l'insieme dei field richiesti all'endpoint. Questo ci collega al punto 4 dell'elenco dei principi visti prima. Con lo stesso endpoint posso costruire richieste specifiche in base alle mie esigenze, senza "sprechi" di campi richiesti e magari rimasti inutilizzati. Con beneficio dell'intero sistema!

Facciamo un altro test, adesso. Modifichiamo nuovamente la nostra richiesta in me?fields=id,name,permissions{permission}. Stavolta stiamo chiedendo, all'endpoint, di recuperare non soltanto l'id ed il nome dell'utente, ma anche i nomi dei singoli permessi. La notazione permissions{permission} più precisamente indica che vogliamo anche un array di oggetti permissions e dentro ogni oggetto una proprietà permission, che in questo caso corrisponde al nome del permesso.

Il risultato? Eccolo:

{
  "id": "10203905429385894",
  "name": "Francesco Malatesta",
  "permissions": {
    "data": [
      {
        "permission": "user_managed_groups"
      },
      {
        "permission": "email"
      },
      ...
    ]
   }
}

Ecco, questa è la "gerarchia" del primo elemento della lista vista prima. La flessibilità vista poco fa vale anche in questo caso. Prova a modificare di nuovo l'URL della richiesta in me?fields=id,name,permissions{permission,status}, e guarda tu stesso il risultato!

GraphQL e Laravel

Siamo su Laravel-Italia, e come giusto che sia una domanda starà prendendo forma nella nostra testa...

Bene, ma posso costruire anche io delle API del genere con Laravel? Come?

Ovviamente si, è possibile costruirle con Laravel: c'è un package appositamente concepito per lo scopo, Folkloreatelier/laravel-graphql, che oggi scopriremo insieme!

D'altronde, come anche la specifica spiega, GraphQL non è un linguaggio di programmazione... semplicemente un linguaggio! Un insieme di regole, appunto. Quello di cui abbiamo bisogno è "qualcosa" che sia capace di interpretare una richiesta scritta seguendo queste regole... tutto qui!

Che poi è esattamente quello che ci permetterà di fare, a breve, il package in questione.

Installazione

La prima cosa da fare ovviamente è aggiungere il package folklore/graphql al nostro file composer.json, con un

$ composer require folklore/graphql

Una volta finita l'installazione, quindi, aggiungiamo

Folklore\GraphQL\ServiceProvider::class,

all'elenco dei service provider, nel file config/app.php. Occupiamoci quindi di registrare la Facade GraphQL nello stesso file.

'GraphQL' => 'Folklore\GraphQL\Support\Facades\GraphQL',

Infine, pubblichiamo il file di configurazione, config/graphql.php tramite

$ php artisan vendor:publish --provider="Folklore\GraphQL\ServiceProvider"

Fatto!

Nota: un altro po' di informazioni interessanti su GraphQL e PHP possono essere trovate sul readme file del repository GitHub di webonyx/graphql-php, di cui il package che abbiamo installato ne è "ponte" con Laravel.

Schema, Query e Mutation

Per lavorare con GraphQL ci sono due concetti fondamentali da conoscere e tenere a mente: la query e la mutation. Come forse il nome già suggerisce, la query è una richiesta che noi inviamo al server per leggere dei dati. D'altra parte, la mutation è una richiesta che ha l'obiettivo di manipolare dei dati.

Niente di complesso insomma: si tratta più che altro di un modo di ragionare specifico al quale non siamo abituati.

Vediamo insieme innanzitutto come costruire una Query per poter ottenere dei dati dal nostro sistema. Dopodichè, passiamo dall'altra parte e analizziamo cosa significa costruire una mutation... e come si fa.

Lo Schema

La lettura di dati, con GraphQL, si fa attraverso una Query. Come già detto in precedenza, una query serve a specificare cosa stiamo richiedendo, e quali campi di quella "cosa" stiamo richiedendo. Inventiamoci un esempio, riprendendo quello visto prima.

Abbiamo questa query:

{
  user(id: 4) {
    name
  }
}

che ha l'obiettivo di ottenere l'utente il cui ID è 4, includendo nell'oggetto ottenuto un campo "name" il cui valore è, appunto, il nome dell'utente. Abbiamo anche detto, prima, che abbiamo la possibilità di modificare questa query in base alla richiesta, senza troppi problemi. Ad esempio,

{
  user(id: 4) {
    name,
    age
  }
}

potrebbe essere una richiesta perfettamente valida. La domanda a questo punto è una: come si fa a definire quali campi possono essere richiesti, e quali no? Cosa rende una query valida, e cosa no?

La soluzione è definire, prima di tutto, uno Schema per la Query che vogliamo creare. Lo possiamo fare creando una nuova classe che estende Folklore\GraphQL\Support\Type. L'esempio presente sul file readme è perfetto: creiamo un file app/GraphQL/Type/UserType.php.

namespace App\GraphQL\Type;

use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Type as GraphQLType;

class UserType extends GraphQLType {

    protected $attributes = [
        'name' => 'User',
        'description' => 'A user'
    ];

    public function fields()
    {
        return [
            'id' => [
                'type' => Type::nonNull(Type::string()),
                'description' => 'The id of the user'
            ],
            'email' => [
                'type' => Type::string(),
                'description' => 'The email of user'
            ]
        ];
    }
}

Il nostro type adesso deve essere registrato. Lo si può fare aggiungendolo nell'array apposito nel file di configurazione config/graphql.php, in types:

'types' => [
    App\GraphQL\Type\UserType::class
],

Bene, ci siamo! Il type è stato definito. Tocca alla query!

La Query

Lo step successivo è costruire una query che faccia uso dello specifico type da noi creato poco fa. Ogni query verrà implementata in una classe specifica, estensione di Folklore\GraphQL\Support\Query.

Ecco la nostra UserQuery.

namespace App\GraphQL\Query;

use GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Query;
use App\User;

class UsersQuery extends Query {

    protected $attributes = [
        'name' => 'users'
    ];

    public function type()
    {
        return Type::listOf(GraphQL::type('User'));
    }

    public function args()
    {
        return [
            'id' => ['name' => 'id', 'type' => Type::string()],
            'email' => ['name' => 'email', 'type' => Type::string()]
        ];
    }

    public function resolve($root, $args)
    {
        if(isset($args['id']))
        {
            return User::where('id' , $args['id'])->get();
        }
        else if(isset($args['email']))
        {
            return User::where('email', $args['email'])->get();
        }
        else
        {
            return User::all();
        }
    }

}

Qui c'è un po' più di roba da analizzare. Facciamo un passo alla volta. Iniziamo da...

public function type()
{
    return Type::listOf(GraphQL::type('User'));
}

Questo metodo si occupa di indicare che type stiamo usando. Abbiamo uno UserType, che identificheremo qui come User. Subito dopo troviamo altri due metodi, args e resolve. Servono a definire tutto quello che riguarda il passaggio di parametri alla query. In questo caso specifico, stiamo implementando la possibilità di ottenere i dati di uno specifico utente partendo da un id, o da un indirizzo email.

Insomma, grazie a questi due metodi, richieste come

{
  user(id: 4) {
    name
  }
}

o

{
  user(email: "ciccio@email.com") {
    name
  }
}

sono perfettamente valide.

Tornando a noi, non rimane altro che registrare la query creata, esattamente come fatto per il type. Stavolta l'elemento dovrà essere aggiunto sotto schemas, sempre nel file di configurazione config/graphql.php:

'schemas' => [
    'default' => [
        'query' => [
            'users' => App\GraphQL\Query\UsersQuery::class
        ],
        // ...
    ]
]

Bene. Anche questa è fatta: prendiamoci una pausa per fare qualche test.

Time for Test!

Se abbiamo implementato tutto correttamente, dovremmo già essere in grado di testare il nostro codice. Come? Con una semplice chiamata: prima, però, ho aggiunto al volo un utente di prova sul database, tramite il DatabaseSeeder.

    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        // $this->call(UsersTableSeeder::class);

        $user = new \App\User();
        $user->name = 'Francesco';
        $user->email = 'francesco@email.com';
        $user->password = \Hash::make('123456');
        $user->save();
    }

Bene, possiamo finalmente provare. Puntiamo a questo URL:

http://graph.dev/graphql?query=query+FetchUsers{users{email}}

dove graph.dev ovviamente è l'hostname scelto per la macchina dove abbiamo installato il progetto. La route /graphql è quella che ci permette di accedere alla nostra API GraphQL. L'URI è definito nel file di configurazione config/graphql.php, alla voce prefix.

Ed ecco il risultato!

{
    "data": {
        "users": [
            {
                "email": "francesco@email.com"
            }
        ]
    }
}

Proviamo ora a chiamare l'URL

http://graph.dev/graphql?query=query+FetchUsers{users{id,email}}

Ecco quale sarà il risultato.

{
    "data": {
        "users": [
            {
                "id": "1",
                "email": "francesco@email.com"
            }
        ]
    }
}

La bellezza delle GraphQL API! Ottenere esattamente ciò che si è chiesto: nulla di più, nulla di meno, con tutta la flessibilità che ne deriva.

Possiamo fare anche altre prove: controlliamo il risultato ottenuto se chiamando

http://graph.dev/graphql?query=query+FetchUsers{users(id:"1"){email}}

o magari...

http://graph.dev/graphql?query=query+FetchUsers{users(id:"2"){email}}

Prima di andare avanti, possiamo fare un esercizio per capire meglio il meccanismo.

Proviamo a raggiungere il seguente URL:

http://graph.dev/graphql?query=query+FetchUsers{users{id,name,email}}

Otterremo un errore, perchè il campo name non esiste. Certo, in realtà esiste ma... non l'abbiamo definito nello UserType! Scopriamo come fare e... rendiamo fattibile anche questa query.

La Mutation

Che cos'è una mutation? Come già detto in precedenza, quando vogliamo leggere dei dati dal nostro sistema possiamo usare le query. Fin qui, tutto regolare. Quando si tratta però di modificare dei dati, le mutation sono lo strumento a cui dobbiamo ricorrere.

Ad ogni modo, la mutation non è niente di più che una query che effettua delle modifiche e può tornare dei dati. La possibilità di ritornare dei dati è molto importante: immaginiamo di avere una mutation che crea un utente. Potrebbe essere comodo fare in modo che ritorni l'id del record appena creato, o magari anche l'indirizzo email per poter essere visualizzato in una view apposita.

Nel contesto del nostro package, creare una mutation significa creare una classe che estende la classe base Folklore\GraphQL\Support\Mutation. Proviamone a creare una il cui scopo è modificare il nome dell'utente: ne inseriremo il codice nel file app/GraphQL/Mutation/UpdateUserNameMutation:

namespace App\GraphQL\Mutation;

use GraphQL;
use GraphQL\Type\Definition\Type;
use Folklore\GraphQL\Support\Mutation;
use App\User;

class UpdateUserNameMutation extends Mutation
{
    protected $attributes = [
        'name' => 'updateUserName'
    ];

    public function type()
    {
        return GraphQL::type('User');
    }

    public function args()
    {
        return [
            'id' => ['name' => 'id', 'type' => Type::nonNull(Type::string())],
            'name' => ['name' => 'name', 'type' => Type::nonNull(Type::string())]
        ];
    }

    public function resolve($root, $args)
    {
        $user = User::find($args['id']);
        if(!$user)
        {
            return null;
        }

        $user->name = $args['name'];
        $user->save();

        return $user;
    }
}

Vediamo, nel dettaglio, cosa succede.

Come già fatto in precedenza, tramite il metodo type stiamo assegnando un type specifico all'operazione. Con il metodo args, invece, stiamo definendo i due argomenti che verranno passati alla mutation per effettuare l'operazione. L'obiettivo è cambiare il nome dell'utente il cui id è quello specificato, quindi gli argomenti da passare saranno:

  • l'id dell'utente;
  • il nuovo nome scelto;

A questo punto, il metodo resolve si occupa di effettuare l'operazione di cui abbiamo bisogno. Se tutto va a buon fine, viene ritornata l'istanza dell'oggetto modificato.

Direi che ci siamo: possiamo provare subito la nostra mutation. Nel browser, puntiamo all'indirizzo

http://graph.dev/graphql?query=mutation+users{updateUserName(id:%221%22,name:%22Ciccio%22){id,name,email}}

ed aspettiamone il risultato, che sarà...

{
    "data": {
        "updateUserName": {
            "id": "1",
            "name": "Ciccio",
            "email": "francesco@email.com"
        }
    }
}

Perfetto! Il nome del nostro utente è stato modificato: da Francesco a Ciccio! Come già anticipato prima, nella richiesta abbiamo anche specificato quali campi vedere in output... comodo no?

Bonus: GraphiQL

Vale la pena menzionare un tool molto comodo, che ci sarà sicuramente d'aiuto in fase di sviluppo. GraphiQL!

Eccolo, in tutto il suo splendore.

GraphiQL permette di provare al volo, sul proprio server, le query e le mutation che abbiamo costruito, per assicurarci che funzionino correttamente. Ancora più interessante è la possibilità, cliccando su "Docs" in alto a destra, di accedere all'elenco di tutte le query e mutation che abbiamo definito per i nostri type.

In poche parole, un tool completo, un must have se si vuole lavorare con GraphQL.

E la buona notizia è che il tool è già installato con il package che stiamo usando! Tutto quello che devi fare per usarlo è raggiungerlo all'indirizzo

http://graph.dev/graphiql

e nulla di più.

Conclusioni

Siamo giunti alla fine di questo articolo dedicato a GraphQL. Ovviamente, l'argomento è vasto e questa non è stata che una semplicissima introduzione. Ci sono tantissime risorse utilissime, nel caso si voglia approfondire: prima tra tutte http://graphql.org/, dove è possibile trovare tutto, dalla documentazione a tantissimi esempi.

Nel caso in cui dovessi decidere di costruire delle GraphQL API, vienicelo a raccontare sullo Slack di Laravel-Italia, ti aspettiamo!