GraphQL appliqué à Symfony
Meetup de l'AFUP Aix/Marseille
20 septembre 2018 @ In Extenso Digital
Text
Qui je suis ?
Samuel Jobard
Développeur @ In Extenso Digital
Aujourd'hui
On ne présentera pas GraphQL
Les solutions pour Symfony
Comment ça marche
Relations et requêtes imbriquées
Mutation
Alors, on en pense quoi ?
Outils et concepts
Merci Didier
Sources
Les solutions pour Symfony
2 bundles principaux pour
On va parler de celui-là
Comment ça marche ?
Rapide exemple basé sur la gestion de véhicules
Modèle de données
Déclarez votre modèle sous forme de types
Car:
type: object
config:
description: "Type of vehicle manage by the app"
fields:
id:
type: "ID!"
description: "Internal identifier"
manufacturer:
type: "String!"
description: "The manufacturer of the car"
model:
type: "String!"
description: "The model of the car"
seats_number:
type: "Int!"
description: "Number of seats in the car"
Query
Rendez accessible votre modèle via une Query
Query:
type: object
config:
fields:
car:
type: 'Car'
args:
id:
description: 'Resolve car using its internal identifier.'
type: 'ID!'
resolve: "@=resolver('Car', [(args['id'])])"
Resolver
Ajoutez votre Resolver
<?php
class CarResolver implements ResolverInterface, AliasedInterface
{
private $carRepository;
public function __construct(CarRepositoryInterface $carRepository)
{
$this->carRepository = $carRepository;
}
public function resolve(string $id): ?CarInterface
{
return $this->carRepository->find($id);
}
public static function getAliases(): array
{
return [
'resolve' => 'Car',
];
}
}
Demandez votre ressource en GraphQL
{
car(id: "Q2FyOmNveA==") {
id
manufacturer
model
seats_number
}
}
GraphQL query
{
"data": {
"car": {
"id": "Q2FyOmNveA==",
"manufacturer": "Volkswagen",
"model": "Coccinelle",
"seats_number": 4
}
}
}
Obtenez votre réponse
GraphiQL
Person:
type: object
config:
description: "Person manage by the app"
fields:
id:
type: "ID!"
description: "Internal identifier"
name:
type: "String!"
description: "Name of the person"
cars:
type: "[Car]"
description: "Cars of the person"
args:
id:
description: 'Resolves car using its identifier.'
type: 'ID'
resolve: "@=service('app.cars.resolver').resolveByPerson(value, args['id'])"
Relations et sous-requêtes
Déclaration de notre nouveau type
Query:
type: object
config:
fields:
car:
type: 'Car'
args:
id:
description: 'Resolve car using its internal identifier.'
type: 'ID!'
resolve: "@=resolver('Car', [(args['id'])])"
person:
type: 'Person'
args:
id:
description: 'Resolves person using its id.'
type: 'ID!'
resolve: "@=resolver('Person', [args['id']])"
Query
Mise à jour de notre query Query
<?php
class CarsResolver implements ResolverInterface, AliasedInterface
{
public function __construct(CarRepository $carRepository)
{
$this->carRepository = $carRepository;
}
public function resolveByPerson(Person $person, string $id = null): ?array
{
$query = new CarsQuery($person->getId(), $id);
return $this->carRepository->findAll($query);
}
public static function getAliases(): array
{
return [
'resolve' => 'Cars',
];
}
}
Resolver
Création du Resolver pour la sous-requête
{
person(id: "UGVyc29uOmR1ZmZ5") {
id
name
cars(id: "VHJ1Y2s6ZmVhcg==") {
id
manufacturer
model
seats_number
}
}
}
GraphQL query
On peut désormais filtrer les véhicules d'une personne
1. Le premier resolver traite la Person
2. Le framework dispatch le résultat au resolver suivant
Mutation:
type: object
config:
fields:
CreateCar:
type: Car!
resolve: "@=mutation('create_car', [args])"
args:
input:
type: CarInput!
Mutation
Notre point d'entrée pour les transactions
CarInput:
type: input-object
config:
fields:
id:
type: "ID!"
manufacturer:
type: "String!"
model:
type: "String!"
seats_number:
type: "Int!"
Chaque ressource est déclarée via un input
<?php
class CarMutation implements MutationInterface, AliasedInterface
{
public function __construct(CarRepositoryInterface $carRepository, ValidatorInterface $validator)
{
$this->carRepository = $carRepository;
$this->validator = $validator;
}
public function createCar(Argument $argument): Car
{
$carInput = VehicleFactory::createCarInput($argument);
$constraintViolations = $this->validator->validate($carInput, null, ['create']);
if ($constraintViolations->count()) {
throw new ValidationException($constraintViolations, 'Car mutation is invalid');
}
return $this->carRepository->save(
VehicleFactory::createCar($carInput)
);
}
public static function getAliases(): array
{
return [
'resolve' => 'CarMutation',
];
}
}
Resolver
mutation {
CreateCar(input: {
id: "Q2FyOnRpbWU=",
manufacturer: "DMC",
model: "DeLorean DMC-12",
seats_number: 4
}) {
id
manufacturer
model
seats_number
}
}
GraphQL query
On peut désormais ajouter un véhicule
Alors, on en pense quoi ?
C'est pas si compliqué
La documentation par le schéma c'est cool
C'est plus explicite pour les usagers
La notion de performance ne doit pas être négligée
Le modèle est fortement lié au schéma
La gestion de la sécurité est un peu plus complexe
Frontend ❤ it !
En optimisant son modèle
Via l'implémentation de cache type Redis
Utilisez les outils de Symfony dans vos resolvers
Overblog propose la gestion des ROLES
Outils et concepts
Créez vos propres types
<?php
class BirthDateType
{
public static function serialize(\DateTime $value): string
{
return $value->format('Y-m-d');
}
public static function parseValue(string $value): \Datetime
{
return new \DateTime($value);
}
public static function parseLiteral(Node $valueNode): string
{
return new \DateTime($valueNode->value);
}
}
BirthDate:
type: custom-scalar
config:
serialize: ["App\\Common\\App\\Type\\BirthDateType", "serialize"]
parseValue: ["App\\Common\\App\\Type\\BirthDateType", "parseValue"]
parseLiteral: ["App\\Common\\App\\Type\\BirthDateType", "parseLiteral"]
Outils et concepts
Etendez votre modèle via les interfaces
Car:
type: object
config:
interfaces: [Vehicle]
description: "Type of vehicle manage by the app"
fields:
id:
type: "ID!"
description: "Internal identifier"
{
vehicles(id: "Q2FyOmNsaW8=") {
id
manufacturer
model
... on Car {
seats_number
}
... on Truck {
maximum_load
}
}
}
Afin de requêter différents types
Outils et concepts
Gérez vos ID via un GlobalID
Car:
type: object
config:
interfaces: [Vehicle]
description: "Type of vehicle manage by the app"
fields:
id:
type: "ID!"
builder: "Relay::GlobalId"
description: "Internal identifier"
Query:
type: object
config:
fields:
car:
type: 'Car'
args:
id:
description: 'Resolves car using its id.'
type: 'String!'
resolve: "@=resolver('Vehicle', [fromGlobalId(args['id'])])"
Utilisez les expressions pour les exploiter
Outils et concepts
Et bien d'autres choses
Enum
Pagination
Upload
Batching
...
GraphQL
Overblog - GraphQL bundle
- https://github.com/overblog/GraphQLBundle
Assoconnect - GraphQL bundle
- https://gitlab.com/assoconnect/graphql-mutation-validator-bundle
- https://medium.com/@syl.fabre/how-to-use-symfony-validator-with-overblog-graphqlbundle-2386a9e6d1fb
Un exemple d'intégration du bundle d'Overblog avec Symfony