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

...

Sources

Merci

Made with Slides.com