Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
GraphQL et Laravel
Mardi 27 août 2019 13:33

Je pratique la programmation depuis les années 80 et j'ai observé de nombreuses évolutions au fil des années. Actuellement on observe de rapides changements au niveau du frontend : système de design, multiples framewoks, systèmes de composants. Côté backend on s'oriente de plus en plus vers le cloud avec des solutions "élastiques" et malheureusement une hégémonie marquée d'Amazon. Dans cet article mon propos va concerner la communication entre client et serveur qui elle aussi connait des évolutions.

De fait, l'API REST (Representational State Transfer) est un standard pour les requêtes HTTP pour faire communiquer un client avec un serveur. Par définition il n'y a pas d'état permanent (stateless). Une requête précise doit retourner ou modifier l'information ciblée. Il existe aussi SOAP (Simple Object Access Protocol) qui est beaucoup moins utilisé.

L'API REST présente des avantages et des inconvénients, il n'existe pas de monde parfait ! On lui reproche un certain nombre de choses comme :

  • obliger à récupérer un ensemble d'informations alors qu'on en désire une seule
  • utiliser plusieurs requêtes pour obtenir toutes les informations désirées
  • n'avoir aucun typage des données
  • ne pas proposer suffisamment de verbes pour affiner les actions et souvent rendre le choix du verbe pas si évident (il n'y a qu'à voir les discussions sur les forums concernant PUT et PATCH)...

J'arrête là la liste mais je pourrais la poursuivre....

On parle de plus en plus maintenant de GraphQL comme remplaçant de REST. Mais c'est quoi GraphQL ? Et peut-on l'utiliser avec Laravel ? Cet article a pour but de répondre à ces deux questions.

GraphQL

GraphQL est a l'origine un produit de Facebook créé en 2012 pour des besoins internes. La première version publique est parue en 2015. En 2018 le projet a été transféré à une fondation.

GraphQL est une spécification. Il y est défini comme un "query language and execution engine", autrement dit un langage de requête et un moteur d'exécution. Vous pouvez donc tout savoir en lisant cette spécification !

Les requêtes

Comme rien ne vaut un exemple en voici un. Supposons que nous voulons récupérer les informations d'un utilisateur sur un serveur. Avec REST on va utiliser une URL du genre users/id avec le verbe GET. Par exemple :

/users/12

Ça va nous retourner les informations de l'utilisateur dont l'id est 12. Par exemple son nom, son email, son âge. Rien ne nous indique a priori quelles informations vont être retournées ni sous quelle forme. Comment fait-on avec GraphQL ? Voici une requête (avec le verbe POST) :

{
  user(id: 12) {
    nom
    email
  }
}
On précise l'id comme avec REST et on demande deux informations précises. On a au retour par exemple :
{
  "user": {
    "nom": "Alain Dupont",
    "email": "dupont@la.fr
  }
}

On voit que tout se passe en JSON. On doit disposer d'un serveur capable de lire et interpréter la requête, d'aller chercher les informations et retourner la réponse. En fait GraphQL peut être implémenté avec n'importe quelle technologie.

Remarquez la symétrie entre la requête et la réponse, elles ont la même structure. Remarquez aussi qu'on obtient ce qu'on a demandé, ni plus, ni moins.

Prolongeons cet exemple en récupérant les articles écrits par cet utilisateur. Avec REST on va prévoir une autre URL du genre users/id/articles, par exemple avec le verbe GET :
/users/12/articles

Et en retour on va avoir tous les articles de l'utilisateur qui a l'id 12. Avec GraphQL on n'a pas besoin d'écrire deux requêtes distinctes pour récupérer d'une part les informations de l'utilisateur et d’autre part ses articles. On écrit une seule requête, par exemple :

{
   User(id: 12) {
       nom
       email
       articles {
          titre
          soustitre
       }
   }
}

On voit qu'on structure la requête comme on veut, il suffit que le serveur soit au courant de cette structure ! On va avoir au retour quelque chose comme ça :

{ "data": 
    { "user": 
        { 
            "nom": "Alain Dupont", 
            "email": "dupont@la.fr", 
            "articles": [ 
              { "titre": "Vive GraphQL", "soustitre": "Une nouvelle aventure" }, 
              { "titre": "REST au placard", "soustitre": "Une tragédie en marche" } 
            ] 
        } 
    } 
}

On voit donc qu'on peut imbriquer facilement des requêtes.

Schéma et types

Côté serveur il faut mettre en place un schéma pour décrire les données qui peuvent être demandées et leur type. On a donc un service qui va effectuer ce travail, il sera écrit dans n'importe quel langage. Pour poursuivre notre exemple voici un schéma possible :

type User {
    nom: String!
    email: String!
    articles: [Article!]!
}
  • User est un objet GraphQL, ce qui signifie qu'il contient des champs
  • nom est un champ de l'objet User, la valeur doit être du type String qui est un des types scalaires disponibles (Int, Float, Boolean... et on peut en créer), le point d'interrogation signifie qu'il ne peut pas y avoir de valeur nulle, autrement dit le serveur doit renvoyer une valeur
  • [Article!] est un tableau d'objets Article
Je pense que vous avez compris le principe. On dispose donc d'un typage qu'on n'a pas avec REST.

Les mutations

On peut effectuer des requêtes en simple lecture mais on peut aussi modifier des valeurs, on parle alors de mutation. Par exemple pour créer un nouvel utilisateur :

mutation {
  createUser(input: {
    nom: "Rodolphe Poitout",
    email: "rodolphe@la.fr"
  }
}
Évidemment côté serveur on doit prévoir un schéma :
input UserInput {
  content: String
  author: String
}

type User {
  nom: String
  email: String
}

type Mutation {
  createUser(input: UserInput): User
}

L'introspection

Lorsqu'on a affaire à une API il vaut mieux disposer d'une bonne documentation pour l'utiliser. Avec GraphQL il suffit de demander au serveur ! C'est l'introspection. pour connaître les types disponibles :

{
  __schema {
    types {
      name
    }
  }
}
On pourrait avoir en retour :
{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "User"
        },
        ...
      ]
    }
  }
}
On peut ainsi interroger les requêtes possibles, le détail des objets... Je ne suis pas entré dans le détail de GraphQL là mais je me suis limité à une courte présentation. pour une approche complète il faut aller sur le site officiel.

GraphQL et Laravel

Installation

J'ai dit que côté serveur on peut utiliser n'importe quelle technologie alors pourquoi pas PHP et en particulier Laravel ? Comme on a de la chance il existe un superbe package : Lighthouse. Si on a déjà un projet construit avec des modèle d'Eloquent il suffit d'ajouter le package, de définir un schéma et c'est parti ! On a un serveur GraphQL fonctionnel ! C'est du moins ce qui est affirmé. On va voir ça avec un exemple...

On va donc partir d'une installation toute fraiche de Laravel :
composer create-project --prefer-dist laravel/laravel graphql

On attend que tout s'installe... Au moment où j'écris ces lignes on en est à la version 5.18.17, la version 6 est proche mais pas encore disponible...

On crée aussi une base de données et on la configure correctement dans le fichier .env. Ensuite on installe Lighthouse :
composer require nuwave/lighthouse
On publie la configuration :
php artisan vendor:publish --provider="Nuwave\Lighthouse\LighthouseServiceProvider"
On se retrouve avec un schéma de base dans graphql/schema.graphql :
"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-01-01 13:00:00`."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

"A date string with format `Y-m-d`, e.g. `2011-05-23`."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")

type Query {
    users: [User!]! @paginate(type: "paginator" model: "App\\User")
    user(id: ID @eq): User @find(model: "App\\User")
}

type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
}

Il faut quand même savoir que Lighthouse met par défaut le schéma en cache, alors pour vous éviter de perdre du temps avec ça je vous conseille une petite purge pour commencer :

php artisan lighthouse:clear-cache
D'autre part dans la configuration (.env) il vaut mieux désactiver le cache tant qu'on fait des essais :
LIGHTHOUSE_CACHE_ENABLE=false

Il existe plusieurs IDE online pour envoyer des requêtes graphQL. Dans le tutoriel de Lighthouse c'est celui-ci qui est utilisé. L'IDE officiel est celui-ci. Mais apparemment le plus utilisé est celui-là. On a donc le choix ! Mais je suis tombé sur des bugs vraiment pénibles alors je me suis finalement tourné vers le client Insomnia :

Il est gratuit, simple à utiliser, complet et efficace !

Les données

Par défaut dans Laravel on a juste une table users. On va ajouter une table articles (avec son modèle) reliée pour enrichir les possibilités :

php artisan make:model -m Article
On va ajouter des champs dans la migration :
public function up()
{
    Schema::create('articles', function (Blueprint $table) {
        $table->bigIncrements('id');
        $table->timestamps();
        $table->unsignedInteger('user_id');
        $table->string('title');
        $table->string('text');
    });
}
On va aussi renseigner la propriété $fillable dans le modèle Article pour autoriser l'assignement de masse :
class Article extends Model
{
    protected $fillable = ['title', 'text'];
}
On envoie les migrations :
php artisan migrate
On va aussi établir une relation pour dire que les utilisateur peuvent avoir plusieurs articles, donc dans le modèle User :
public function articles()
{
    return $this->hasMany(Article::class);
}
On va remplir un peu ces tables. Par défaut on a déjà un factory pour les users, on va en ajouter un pour les articles :
php artisan make:factory ArticleFactory --model=Article
Avec ce code :
$factory->define(Article::class, function (Faker $faker) {
    return [
        'title' => $faker->sentence($nbWords = 3, $variableNbWords = true),
        'text' => $faker->paragraph($nbSentences = 3, $variableNbSentences = true),
    ];
});
On va créer quelques enregistrement (3 utilisateurs et 5 articles pour chacun) :
php artisan tinker
factory(App\User::class, 3)->create()->each(function ($user) {$user->articles()->createMany(factory(App\Article::class, 5)->make()->toArray());});

Le schéma et les requêtes

C'est maintenant qu'on entre dans le vif du sujet ! Il faut modifier le fichier graphql/schema.graphql pour le rendre cohérent avec nos modèles. On va commencer tranquillement pour voir si tout fonctionne avec ce schéma de base :

"A datetime string with format `Y-m-d H:i:s`, e.g. `2018-01-01 13:00:00`."
scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")

"A date string with format `Y-m-d`, e.g. `2011-05-23`."
scalar Date @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\Date")

type Query {
    users: [User!]! @all
}

type User {
    id: ID!
    name: String!
    email: String!
    created_at: DateTime!
    updated_at: DateTime!
 }

On crée la requête users qui doit renvoyer tous les utilisateurs avec le type User. Remarquez la directive @all qui indique justement qu'on veut tous les enregistrements (pour connaître toutes les directives disponibles c'est ici). Ce n'est pas une directive standard de Graphql mais de Lighthouse. c'est là que le package est bien pratique parce qu'il s'occupe de tous les traitements avec Eloquent.

On utilise Insomnia pour envoyer une requête :

On a demandé le nom et l'email pour tous les utilisateurs, on obtient bien ces informations. Remarquez qu'on peut demander le schéma (introspection) :

Et si on clique sur User : Maintenant si on veut récupérer un utilisateur de façon individuelle il faut compléter le schéma :
type Query {
    users: [User!]! @all
    user(id: Int! @eq): User @find
}
On découvre deux nouvelles directives : @eq et @find. Avec ce résultat : Là où ça devient plus intéressant c'est avec les relations, on complète le type User avec la directive @hasMany :
type User {
    ...
    articles: [Article!]! @hasMany
}

type Article {
    id: ID!
    title: String!
    text: String!
}
On a aussi créé le modèle Article. On peut maintenant demander les articles (avec juste le titre) d'un utilisateur particulier : Pratique non ? On peut aussi prévoir la pagination, par exemple pour les articles avec la directive @paginate :
type Query {
    ...
    articles: [Article!]! @paginate
}
On peut alors écrire ce genre de requête :

Mais là on perd la magie de Laravel pour générer les liens de pagination, il faut gérer ça complètement côté client.

Les mutations

Évidemment Graphql ne se limite pas à lire des informations, on peut aussi faire des ajouts ou des modifications, on parle alors de mutation. Complétons le schéma pour créer des utilisateurs avec la directive @create :

type Mutation {
    createUser(
        name: String!
        email: String!
        password: String!
    ): User! @create
}
De la même manière on peut modifier un enregistrement avec la directive @update :
type Mutation {
    updateName(id: ID!, name: String!): User! @update
}
Et enfin on peut supprimer un enregistrement :
type Mutation {
  deleteUser(id: ID!): User @delete
}

La validation

Qu'en est-il de la validation ? reprenons le cas ci-dessus de création d'un utilisateur en ajoutant une validation :
type Mutation {
    createUser(
        name: String! @rules(apply: ["required", "min:8", "max:20"])
        email: String! @rules(apply: ["email"])
        password: String!
    ): User! @create
}
On utilise la directive @rules et on ajoute les règles désirées dans un tableau. On va voir si ça fonctionne :

On voit qu'on a bien une erreur de validation retournée avec toutes les informations nécessaires. On peut prévoir une message personnalisé :

name: String! @rules(apply: ["required", "min:8", "max:20"], , messages: { min: "Faudrait quand même pas exagérer !"})
Par contre je n'ai pas creusé l'affaire concernant l'internationalisation...

Conclusion

On voit que Graphql est correctement pris en charge avec Lighthouse pour les utilisateurs de Laravel. Mais on voit aussi que ça change pas mal de choses dans la philosophie du système. On n'a plus de routes mais un seul point d'entrée. La pagination est à reprendre en charge côté client. La validation est aussi à reconsidérer. Je n'ai pas parlé de l'authentification qui passe mieux en version API...

En conclusion Graphql est bien plus cohérent que REST et constitue une avancée considérable dans le domaine. Je ne pense pas que ce soit son remplaçant mais il constitue une possibilité à utiliser en parallèle. Sans doute certaines situations conviennent mieux à l'un ou à l'autre. Par exemple une API parfaitement CRUD se gère élégamment avec REST.

Dans cet article je n'ai abordé que des cas simples et je pense qu'on doit se prendre un peu la tête dans certains cas. Mais la documentation est épaisse et plutôt bien faite. On peut se créer ses propres directives. Il y a aussi des helpers pour les tests unitaires.

Vous en pensez quoi et êtes-vous prêt à l'utiliser ?


Par bestmomo

Aucun commentaire