Commentaires
Une API GraphQL avec Symfony
Créée par Facebook, GraphQL est une syntaxe pour requêter une API. Son standard a été quant à lui ouvert en 2015.
C’est avant tout une manière différente de concevoir une API ; une architecture REST par exemple propose d’utiliser plusieurs endpoints qui standardisent la façon de recevoir des données.
L’approche de GraphQL est de n’utiliser qu’un seul endpoint mais qui est capable de comprendre les données demandées et de les fournir de la façon souhaitée.
Ainsi, une requête GraphQL qui demandent tous les articles d'un blog, accompagnés des nom et avatar de toutes les personnes aimant ces articles, peut ressembler à cela :
query {
posts {
title
likes {
name
avatar
}
}
}
Et donnerait le résultat suivant :
{
"data": {
"posts": [
{
"title": "My first post",
"likes": [
{
"name": "Sacha",
"avatar": "sacha.jpg"
},
{
"name": "Mike",
"avatar": "mike.jpg"
}
]
},
{
"title": "Another great post",
"likes": []
}
]
}
}
Cet exemple illustre ce qui m’a séduit dans l’approche de GraphQL : la facilité avec laquelle on peut agréger des données de différentes sources, le tout au sein d’une même requête HTTP. Une architecture REST nous contraindrait à faire une requête HTTP supplémentaire par article, à implémenter un endpoint dédié ou encore à utiliser une option de type include pour récupérer ces données.
Je vous invite à parcourir le site graphql.org pour vous faire votre propre avis ! La suite de l’article va présenter l’implémentation de la requête GraphQL vue plus haut.
L'implementation
Créons notre projet Symfony 4 :
composer create-project symfony/skeleton hello-graphql
composer config extra.symfony.allow-contrib true
composer require overblog/graphql-bundle # Le bundle que je vous recommande
composer require --dev overblog/graphiql-bundle # Ajoute une interface graphique
L'implémentation d'une API GraphQL se déroule en 2 étapes :
Étape 1 : Définir les types qui correspondent aux données à exposer
Notre requête précédente comportait un type User
et un type Post
, ces deux types représentent des objets dont nous allons lister chacun des champs.
Chaque champ doit être strictement typé en utilisant un type scalaire (Int, Float, String, Boolean, ID
), un tableau ou un autre type… L’utilisation d’un ! indique une valeur non null.
Le bundle nous permet de décrire nos types en utilisant le langage YAML.
Créez un nouveau fichier config/graphql/types/User.types.yaml
:
User:
type: object
config:
fields:
name:
type: "String!"
avatar:
type: "String"
Ainsi qu'un nouveau fichier
config/graphql/types/Post.types.yaml
:
Post:
type: object
config:
fields:
title:
type: "String!"
likes:
type: "[User!]!"
Maintenant que nous avons décrit nos types à GraphQL, nous devons lui expliquer comment résoudre chacun des champs ! C’est l’objectif d’un resolveur. Par défaut durant la phase d'exécution le bundle va utiliser le composant PropertyAccess afin que nous n'ayons pas besoin d’expliquer comment résoudre des champs triviaux comme name
, avatar
, ou encore title
.
On considérera que ces champs peuvent être résolus en faisant appel à des accesseurs sur nos entités.
En revanche on va expliquer à GraphQL comment résoudre le champ likes
qui récupère les utilisateurs ayant aimé un article de blog.
Pour cela on va utiliser l’ExpressionLanguage via la clé resolve
et expliquer comment appeler notre resolveur.
Modifions config/graphql/types/Post.types.yaml
:
# On ajoute la clé resolve
likes:
type: "[User!]!"
resolve: "@=resolver('postLikes', [value])"
La signature pour appeler un resolveur est resolver(string $alias, array $args = [])
.
Ainsi postLikes
est un alias que nous donnons à notre resolveur qui recevra comme unique argument value
qui est l’objet Post.
Définissons alors le resolveur qui correspond à l’alias postLikes
:
services:
App/GraphQL/Resolver/PostResolver:
# Injectez les arguments nécessaires...
tags:
- { name: overblog_graphql.resolver, alias: "postLikes", method: "resolveLikes" }
Enfin on implémente notre resolveur qui est une pure fonction PHP avec le métier de notre application :
class PostResolver
{
// constructeur ...
public function resolveLikes(Post $post): Collection
{
return $this->userRepository->findUsersLikingPost($post);
}
}
Étape 2 : Définir le type Query
Le type Query est un objet dont chaque champ peut être demandé à la racine d'une requête, c'est assez proche d'un endpoint en architecture REST.
Dans notre cas, nous n'avons qu'un seul champ (posts
) qui permet d'obtenir la liste de tous les articles.
Créez un nouveau fichier config/graphql/types/Query.types.yaml
:
Query:
type: object
config:
fields:
posts:
type: "[Post!]!"
resolve: "@=resolver('allPosts')"
Il ne nous reste plus qu’à ajouter l’alias allPost
à notre PostResolver
et son implémentation :
// ...
public function resolveAll(): Collection
{
return $this->postRepository->findAll();
}
C’est tout pour l’implémentation !
Conclusion
Récapitulons l'exécution de la requête GraphQL :
L'exécution se fait de haut en bas. Ainsi le resolveur Query.posts
est exécuté en premier, puis pour chacun de ses résultats les résolveurs Post.title
et Post.likes
sont appelés. Enfin pour chacun des résultats de Post.likes
, les resolveurs User.name
et User.avatar
sont appelés.
Evidemment il vous reste beaucoup de choses à découvrir avant de pouvoir mettre votre API en production, comme les arguments qui permettent d’affiner vos requêtes, les mutations, la spécification Relay, les problématiques d’optimisation, cache et de sécurité…
Malgré tout, bienvenue dans le monde merveilleux de GraphQL !