GraphQL appliqué à Symfony
Meetup de l'AFUP Aix/Marseille
20 septembre 2018 @ In Extenso Digital
Text
Samuel Jobard
Développeur @ In Extenso Digital
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
2 bundles principaux pour
On va parler de celui-là
Rapide exemple basé sur la gestion de véhicules
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"
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'])])"
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
}
}
{
"data": {
"car": {
"id": "Q2FyOmNveA==",
"manufacturer": "Volkswagen",
"model": "Coccinelle",
"seats_number": 4
}
}
}
Obtenez votre réponse
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'])"
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']])"
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',
];
}
}
Création du Resolver pour la sous-requête
{
person(id: "UGVyc29uOmR1ZmZ5") {
id
name
cars(id: "VHJ1Y2s6ZmVhcg==") {
id
manufacturer
model
seats_number
}
}
}
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!
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',
];
}
}
mutation {
CreateCar(input: {
id: "Q2FyOnRpbWU=",
manufacturer: "DMC",
model: "DeLorean DMC-12",
seats_number: 4
}) {
id
manufacturer
model
seats_number
}
}
On peut désormais ajouter un véhicule
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
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"]
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
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
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