Introduction

GraphQL c'est quoi

GraphQL est un langage de requête initié par Facebook en 2012 et développé en 2015. Facebook Manifest. GraphQL permet de se brancher à n'importe quel type de base de données ou d'API. Le but de GraphQL est de décrire les données et les fonctions disponibles entre les applications client-serveur.

GraphQL ne stocke donc pas de données. Il va seulement décrire le modèle donnée et savoir comment aller la récupérer sur vos différentes applications backend.

Je vous invite à lire l'article de notre blog expliquant comment fonctionne GraphQL.

Qu'allons nous faire ?

Dans ce tutoriel nous allons mettre en place un serveur GraphQL sur Symfony 4 en utilisant le bundle https://github.com/overblog/GraphQLBundle

Le but est de comprendre:

  • la mise en place d'un serveur GraphQL
  • la création des requêtes pour lire la donnée
  • la création des requêtes d'écriture des données

Pré-requis

Nous allons utiliser une base de données MySQL pour le stockage des données.

Le serveur sera en Symfony 4, avec la version 7 de PHP.

Si vous ne souhaitez pas installer node sur votre machine, vous pouvez utiliser Docker. Le code fourni pour le tutoriel est disponible ici, contient un fichier docker-compose.yml vous permettant d'installer le projet.

Installation du serveur GraphQL

Installation de docker

Si vous souhaitez utiliser Docker, je vous invite à cloner le projet github.

Une fois cloné vous pouvez lancer :

docker-compose up -d

vous devez aussi ajouter la ligne suivante dans votre /etc/hosts :

127.0.0.1 symfony.local

Si vous travaillez dans le container docker, les commandes suivantes doivent être lancées dans la machine docker :

docker-compose exec php sh

Installer Symfony

Comme nous utilisons un Symfony 4, nous allons mettre en place flex.

Pour l'installation vous n'avez qu'à lancer la commande suivante :

composer create-project symfony/skeleton symfony

Si tout est ok, vous devriez avoir la page de base de Symfony à l'adresse suivante http://symfony.localhost/

symfony

Retrouvez le code directement ici

Configuration de la BDD

Création de la base de données

Si vous utilisez le container docker, la base de données MySQL est comprise dans le projet.

Si vous n'utilisez pas le docker, vous devez installer un MySQL sur votre machine via la documentation suivante

Création du schéma

Nous allons utiliser doctrine pour mettre en place le schéma de la base de données.

Vous devez installer doctrine et maker (qui permet de générer les entity) en lançant :

composer require doctrine maker

Vous pouvez changer le fichier .env avec la connexion à votre base de données. Dans le cadre de l'utilisation du container docker vous devez mettre :

###> doctrine/doctrine-bundle ### # Format described at http://docs.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url # For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db" # Configure your db driver and server_version in config/packages/doctrine.yaml DATABASE_URL=mysql://symfony:symfony@db:3306/symfony ###< doctrine/doctrine-bundle ###

Puis nous allons créer le schéma de base de données. Dans la suite du tutoriel nous allons imaginer que l'application doit gérer les astronautes d'Eleven Labs, les liens avec leurs planètes et leurs grades.

Nous allons créer les trois entités doctrine.

Astronautes

Lancez la commande :

php bin/console make:entity Astronaut

Cela va créer le fichier src/Entity/Astronaut.php.

Vous pouvez alors ajouter les fields que nous aurons besoin :

<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\AstronautRepository") */ class Astronaut { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string") */ private $pseudo; /** * @ORM\ManyToOne(targetEntity="Grade") * @ORM\JoinColumn(name="grade_id", referencedColumnName="id") */ private $grade; /** * Get the value of grade */ public function getGrade() { return $this->grade; } /** * Set the value of grade * * @return self */ public function setGrade($grade) { $this->grade = $grade; return $this; } /** * Get the value of pseudo */ public function getPseudo() { return $this->pseudo; } /** * Set the value of pseudo * * @return self */ public function setPseudo($pseudo) { $this->pseudo = $pseudo; return $this; } /** * Get the value of id */ public function getId() { return $this->id; } }

Planets

Lancez la commande :

php bin/console make:entity Planet

Cela va créer le fichier src/Entity/Planet.php.

Vous pouvez alors ajouter les fields dont nous aurons besoin :

<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\PlanetRepository") */ class Planet { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string") */ private $name; /** * @ORM\ManyToMany(targetEntity="Astronaut") * @ORM\JoinTable(name="planet_astronaut", * joinColumns={@JoinColumn(name="planet_id", referencedColumnName="id")}, * inverseJoinColumns={@JoinColumn(name="astronaut_id", referencedColumnName="id", unique=true)} * ) */ private $astronauts; public function __construct() { $this->astronauts = new \Doctrine\Common\Collections\ArrayCollection(); } public function getAstronauts() { return $this->astronauts; } public function setAstronauts($astronauts) { $this->astronauts = $astronauts; return $this; } /** * Get the value of name */ public function getName() { return $this->name; } /** * Set the value of name * * @return self */ public function setName($name) { $this->name = $name; return $this; } /** * Set the value of id * * @return self */ public function setId($id) { $this->id = $id; return $this; } }

Grade

Lancez la commande :

php bin/console make:entity Grade

Cela va créer le fichier src/Entity/Grade.php.

Vous pouvez alors ajouter les fields dont nous aurons besoin :

<?php namespace App\Entity; use Doctrine\ORM\Mapping as ORM; /** * @ORM\Entity(repositoryClass="App\Repository\GradeRepository") */ class Grade { /** * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") */ private $id; /** * @ORM\Column(type="string") */ private $name; /** * Get the value of name */ public function getName() { return $this->name; } /** * Set the value of name * * @return self */ public function setName($name) { $this->name = $name; return $this; } /** * Get the value of id */ public function getId() { return $this->id; } }

Création de la base

Nous allons utiliser les commandes de doctrine.

Il faut d'abord créer la migration en lançant :

php bin/console doctrine:migrations:generate

Cela va générer un fichier dans le dossier src/Migrations

Puis il faut lancer la migration pour créer les tables :

php bin/console doctrine:migrations:migrate

C'est bon ! Vos tables sont créés.

Retrouvez le code directement ici

Création des types GraphQL

Installation du bundle

Commençons par installer le bundle https://github.com/overblog/GraphQLBundle

composer require overblog/graphql-bundle

Nous ajoutons au même moment l'IDE GraphiQL qui est contenu dans un autre bundle https://github.com/overblog/GraphiQLBundle. L'IDE permet d'afficher directement la documentation, ainsi que d'effectuer les query :

composer req --dev overblog/graphiql-bundle

Normalement l'url http://symfony.localhost/graphiql est disponible (avec une erreur 500)

Types objet

Nous allons commencer par créer les types GraphQL pour les trois principaux objets :

  • Astronaute
  • Planète
  • Grade

Nous allons ensuite mettre les types dans le dossier config/graphql/types.

Grade

On commence par grade qui est l'objet le plus simple, il ne contient que le nom du grade.

Ajoutez le fichier Grade.yaml avec le code suivant :

Grade: type: object config: fields: id: type: 'Int!' name: type: 'String!'

Planète

Ajoutez le fichier Planet.yaml avec le code suivant :

Planet: type: object config: fields: id: type: 'Int!' name: type: 'String!' astronauts: type: '[Astronaut]'

Comme vous le remarquez, le type GraphQL ne suit pas directement le type MySQL. Ici on permet la récupération directement dans l'object planet de l'ensemble des astronautes.

Astronaute

Ajoutez le fichier Astronaut.yaml avec le code suivant :

Astronaut: type: object config: fields: id: type: 'Int!' pseudo: type: 'String!' grade: type: 'Grade' planet: type: 'Planet'

Dans le cas de l'astronaute, l'objet contient directement le grade et la planet.

Retrouvez le code directement ici

Resolver des queries

Création du type Query

Avant de mettre en place les resolvers pour les query en lecture. Vous devez créer le type query.

Il faut ensuite créer un type avec l'ensemble des fonctions que vous souhaitez avoir. Nous allons :

  • récupérer l'ensemble des astronautes ;
  • récupérer un astronaute ;
  • récupérer une planète.

Commençons par créer le fichier Query.yaml dans le dossier config/graphql/types.

Dans ce fichier nous allons identifier les points d'entrée du graphql :

Query: type: object config: fields: Astronaut: type: 'Astronaut' args: id: description: 'Resolves Astronaut using its id.' type: 'Int!' Astronauts: type: '[Astronaut]' Planet: type: 'Planet'

Si tout est ok, vous devez avoir la documentation qui s'affiche dans l'interface GraphiQL

Création des resolvers

Si vous essayez la query :

{ Astronauts { id } }

Vous devriez voir la réponse suivante :

{ "data": { "Astronauts": null } }

Puisque pour l'instant vous n'avez aucun resolver.

Le resolver est le code qui permet de récuperer la donnée dans le base.

Dans le bundle il s'agit de service symfony.

Il existe deux façons de créer un resolver :

  • en utilisant des services implémentant les interfaces ResolverInterface, AliasedInterface ;
  • en créant ses propres services.

On va commencer par créer les trois resolver via les interfaces.

Dans le fichier Query.yaml vous devez ajouter les appels aux différents resolver :

Query: type: object config: fields: Astronaut: type: 'Astronaut' args: id: description: 'Resolves Astronaut using its id.' type: 'Int!' resolve: "@=resolver('Astronaut', [args['id']])" Astronauts: type: '[Astronaut]' resolve: "@=resolver('Astronauts')" Planet: type: 'Planet' args: id: description: 'Resolves Planet using its id.' type: 'Int!' resolve: "@=resolver('Planet', [args['id']])"

Puis nous allons créer les services. Créez le dossier src/Resolver.

Ajoutez le fichier PlanetResolver.php avec :

<?php namespace App\Resolver; use App\Repository\PlanetRepository; use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface; use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; final class PlanetResolver implements ResolverInterface, AliasedInterface { /** * @var PlanetRepository */ private $planetRepository; /** * * @param PlanetRepository $planetRepository */ public function __construct(PlanetRepository $planetRepository) { $this->planetRepository = $planetRepository; } /** * @return \App\Entity\Planet */ public function resolve(int $id) { return $this->planetRepository->find($id); } /** * {@inheritdoc} */ public static function getAliases(): array { return [ 'resolve' => 'Planet', ]; } }

Ajoutez le fichier AstronautResolver.php avec :

<?php namespace App\Resolver; use App\Repository\AstronautRepository; use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface; use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; final class AstronautResolver implements ResolverInterface, AliasedInterface { /** * @var AstronautRepository */ private $astronautRepository; /** * * @param AstronautRepository $astronautRepository */ public function __construct(AstronautRepository $astronautRepository) { $this->astronautRepository = $astronautRepository; } /** * @return \App\Entity\Planet */ public function resolve(int $id) { return $this->astronautRepository->find($id); } /** * {@inheritdoc} */ public static function getAliases(): array { return [ 'resolve' => 'Astronaut', ]; } }

Ajoutez le fichier AstronautsResolver.php avec :

<?php namespace App\Resolver; use App\Repository\AstronautRepository; use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface; use Overblog\GraphQLBundle\Definition\Resolver\ResolverInterface; final class AstronautsResolver implements ResolverInterface, AliasedInterface { /** * @var AstronautRepository */ private $astronautRepository; /** * * @param AstronautRepository $astronautRepository */ public function __construct(AstronautRepository $astronautRepository) { $this->astronautRepository = $astronautRepository; } /** * @return \App\Entity\Astronaut */ public function resolve() { return $this->astronautRepository->findAll(); } /** * {@inheritdoc} */ public static function getAliases(): array { return [ 'resolve' => 'Astronauts', ]; } }

Si tout est ok la réponse à votre requête est :

{ "data": { "Astronauts": [] } }

Et si vous ajoutez des astronautes dans votre base de donnnées et changez la requête en :

{ Astronauts { id, pseudo, grade { id, name } planet { id, name } } }

La réponse devrait être :

{ "data": { "Astronauts": [ { "id": 1, "pseudo": "captainjojo", "grade": { "id": 1, "name": "admiral" }, "planet": null }, { "id": 2, "pseudo": "pouzor", "grade": { "id": 2, "name": "rookie" }, "planet": null }, { "id": 3, "pseudo": "francki", "grade": { "id": 2, "name": "rookie" }, "planet": null } ] } }

Il manque le lien avec la planète, car dans un objet Astronaut nous n'avons pas directement le lien avec la planet.

Nous allons donc mettre en place un autre resolver. Cette fois-ci via un service.

Dans le fichier Astronaut.yaml nous allons ajouter le resolver pour la planète :

Astronaut: type: object config: fields: id: type: 'Int!' pseudo: type: 'String!' grade: type: 'Grade' planet: type: 'Planet' resolve: "@=service('planet.resolver').resolveInAstronaut(value, args, context, info)"

Puis dans le PlanetResolver.php vous pouvez ajouter la fonction de resolve suivante :

public function resolveInAstronaut(Astronaut $astronaut, $args, $context, $info) { return $this->planetRepository->findByAstronaut($astronaut->getId()); }

Vous devez ajouter la fonction suivante dans le PlanetRepository.php :

public function findByAstronaut($id) { return $this->createQueryBuilder('p') ->innerJoin('p.astronauts', 'a') ->andWhere('a.id = :id') ->setParameter('id', $id) ->getQuery() ->getOneOrNullResult(); }

Si tout est bon le résultat de votre précédente requête doit être :

{ "data": { "Astronauts": [ { "id": 1, "pseudo": "captainjojo", "grade": { "id": 1, "name": "admiral" }, "planet": { "id": 1, "name": "duck" } }, { "id": 2, "pseudo": "pouzor", "grade": { "id": 2, "name": "rookie" }, "planet": { "id": 2, "name": "panda" } }, { "id": 3, "pseudo": "francki", "grade": { "id": 2, "name": "rookie" }, "planet": { "id": 2, "name": "panda" } } ] } }

Retrouvez le code directement ici

Resolver des mutations

Création d'un type mutation

Comme pour la query, nous devons définir les mutations possibles. Il s'agit là aussi d'une fonction prenant en entrée un type input et qui renvoie un objet.

Type Input

Pour ce tutoriel nous allons seulement créer un nouvel astronaute. Nous avons donc besoin d'un seul type input pour l'astronaute.

Dans le dossier config/graphql/types vous devez ajouter un fichier AstronautInput.yaml qui contient :

AstronautInput: type: input-object config: fields: pseudo: type: 'String!'

Ajout de la mutation

Dans le dossier config/graphql/types vous devez ajouter un fichier Mutation.yaml qui contient :

MutationSuccess: type: object config: fields: content: type: String! Mutation: type: object config: fields: NewAstronaut: type: MutationSuccess resolve: "@=mutation('NewAstronaut', [args['input']['pseudo']])" args: input: type: AstronautInput!

Puis dans la configuration du bundle vous devez definir le point d'entrée du type mutation. Dans le fichier `config/graphql.yaml`` :

overblog_graphql: definitions: schema: query: Query mutation: Mutation mappings: auto_discover: false types: - type: yaml dir: "%kernel.project_dir%/config/graphql/types" suffix: ~

Resolver de mutation

Comme pour les resolvers de query, il s'agit d'un service qui implémente les interfaces MutationInterface, AliasedInterface.

Créez le dossier src/Mutation et ajouter le fichier AstronautMutation.php avec ceci :

<?php namespace App\Mutation; use Doctrine\ORM\EntityManagerInterface; use Overblog\GraphQLBundle\Definition\Resolver\AliasedInterface; use Overblog\GraphQLBundle\Definition\Resolver\MutationInterface; use App\Entity\Astronaut; final class AstronautMutation implements MutationInterface, AliasedInterface { private $em; public function __construct(EntityManagerInterface $em) { $this->em = $em; } public function resolve(string $pseudo) { $astronaute = new Astronaut(); $astronaute->setPseudo($pseudo); $this->em->persist($astronaute); $this->em->flush(); return ['content' => 'ok']; } /** * {@inheritdoc} */ public static function getAliases(): array { return [ 'resolve' => 'NewAstronaut', ]; } }

Il ne vous reste plus qu'à configurer le service. Dans le fichier config/services.yaml :

App\Mutation\: resource: '../src/Mutation' tags: ['overblog_graphql.mutation']

Testons

Dans GraphiQL vous pouvez mettre la query suivante :

mutation NewAstronaut($astronaute: AstronautInput!) { NewAstronaut(input: $astronaute) { content } }

Puis dans query variables en bas à gauche :

{ "astronaute": { "pseudo": "test" } }

Si tout est ok pour devriez avoir cela comme réponse :

{ "data": { "NewAstronaut": { "content": "ok" } } }

Retrouvez le code directement ici

Conclusion

Je vous invite à lire la documentation de GraphQL et du bundle https://github.com/overblog/GraphQLBundle/ pour voir l'ensemble des fonctionnalités disponibles dans GraphQL.