Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Concevoir son API pour le futur

Concevoir son API pour le futur

Titouan Galopin

March 24, 2023
Tweet

More Decks by Titouan Galopin

Other Decks in Technology

Transcript

  1. Concevoir son API
    pour le futur
    Inspirations de Symfony
    Titouan Galopin

    View Slide

  2. Titouan Galopin
    Symfony Core Team member (Symfony UX)
    Tech Lead
    2

    View Slide

  3. Introduction
    3
    Notre challenge à Selency

    View Slide

  4. Agenda
    ● De quoi parle-t-on ?
    ● Déconnecter l’interne de l’externe
    ● Communiquer le changement
    ● Documenter pour expliquer
    4

    View Slide

  5. 1.
    De quoi parle-t-on ?
    Quelles sont les difficultés dans la
    conception d’une API moderne ?

    View Slide

  6. 6
    Une API est un contrat

    View Slide

  7. 7
    Des consommateurs
    (Web, mobile, desktop, autres API, …)
    dépendent de notre API

    View Slide

  8. 8
    Différentes personnes travaillent
    autour de ce contrat

    View Slide

  9. 9
    Concevoir une API
    ~=
    Concevoir une librairie PHP
    réutilisable

    View Slide

  10. 10
    ● Rester stable dans le temps
    ● Créer des chemins de migration
    ● Communiquer aux consommateurs

    View Slide

  11. 11
    On a de la chance :
    Symfony a déjà créé ces processus !
    Et si on les réutilisait ?

    View Slide

  12. 2.
    Déconnecter l’interne
    de l’externe
    Ne pas exposer le format interne
    de données

    View Slide

  13. 13
    Être stable
    =>
    Découpler les inputs/output API
    de la logique interne

    View Slide

  14. 14
    Output
    Les transformers

    View Slide

  15. 15
    Pas de serializer automatique !
    Si l’entité change, l’output ne doit pas changer

    View Slide

  16. 16
    Transformer
    =
    Serializer manuel + Sous-ressources
    Exemple : League Fractal
    https://fractal.thephpleague.com
    (=> nous avons notre propre système à Selency)

    View Slide

  17. 17
    Base de données
    Controller &
    Entité
    Transformer Output API

    View Slide

  18. 18
    Base de données
    Controller &
    Entité
    Transformer Output API
    Si le code change dans le controller
    ou l’entité, le transformer permet de
    garder le même output

    View Slide

  19. 19
    class BookTransformer extends Fractal\TransformerAbstract
    {
    public function transform(Book $book)
    {
    return [
    'id' => (int) $book->getId(),
    'title' => $book->getTitle(),
    'year' => (int) $book->getYear(),
    'links' => [
    [
    'rel' => 'self',
    'uri' => '/books/'.$book->getId(),
    ]
    ],
    ];
    }
    }

    View Slide

  20. 20
    class BookTransformer extends Fractal\TransformerAbstract
    {
    public function transform(Book $book)
    {
    return [
    'id' => (int) $book->getId(),
    'title' => $book->getTitle(),
    'year' => (int) $book->getYear(),
    'links' => [
    [
    'rel' => 'self',
    'uri' => '/books/'.$book->getId(),
    ]
    ],
    ];
    }
    }

    View Slide

  21. 21
    Les transformers permettent aussi de gérer les
    links, sous-ressources, …
    => cf. League Fractal

    View Slide

  22. 22
    Input
    Utilisez des Data Transfer Object

    View Slide

  23. 23
    Pas de deserializer directement dans l’entité !
    Risques :
    si l’entité change => incompatibilité
    si input invalide => entité invalide + TypeError si
    propriété typée

    View Slide

  24. 24
    Input API DTO
    Controller &
    Entité
    Base de données
    Utilisez des DTO dédiés

    View Slide

  25. 25
    Input API DTO
    Controller &
    Entité
    Base de données
    Si le code change dans le controller
    ou l’entité, le DTO permet de garder
    le même input
    Utilisez des DTO dédiés

    View Slide

  26. 26
    class AuthRegisterPayload
    {
    #[Assert\Email(mode: Email::VALIDATION_MODE_STRICT)]
    #[Assert\NotBlank]
    public $email;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $firstName;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $lastName;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $password;
    }

    View Slide

  27. 27
    class AuthRegisterPayload
    {
    #[Assert\Email(mode: Email::VALIDATION_MODE_STRICT)]
    #[Assert\NotBlank]
    public $email;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $firstName;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $lastName;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $password;
    }

    View Slide

  28. 28
    $payload = $this->serializer->deserialize(
    data: $request->getContent(),
    type: AuthRegisterPayload::class,
    format: 'json'
    );
    $errors = $this->validator->validate($payload);
    if ($errors->count() > 0) {
    throw new PayloadValidationException($errors);
    }

    View Slide

  29. 29
    Bonus : pas d’entités anémiques

    View Slide

  30. 30
    $user = new User();
    $user->setEmail($email);
    $user->setFirstName($firstName);
    $user->setLastName($lastName);
    $user->setPassword($hashedPassword);
    Entité anémique : difficile à maintenir

    View Slide

  31. 31
    $user = new User();
    $user->setEmail($email);
    $user->setFirstName($firstName);
    $user->setLastName($lastName);
    $user->setPassword($hashedPassword);
    Entité anémique : difficile à maintenir
    $user n’est initialement pas valide

    View Slide

  32. 32
    $user = new User();
    $user->setEmail($email);
    $user->setFirstName($firstName);
    $user->setLastName($lastName);
    $user->setPassword($hashedPassword);
    Entité anémique : difficile à maintenir
    $user n’est initialement pas valide
    Est-ce qu’on est sûr qu’on update
    bien tous les bons champs ?

    View Slide

  33. 33
    class User
    {
    public static function createFromPayload(AuthRegisterPayload $payload): self
    {
    // ...
    }
    public function applyUpdatePayload(UserUpdatePayload $payload)
    {
    // ...
    }
    }
    A la place, payloads + entités riches

    View Slide

  34. 34
    // RegistrationController
    $payload = $this->createPayload($request->getContent(), AuthRegisterPayload::class);
    $user = User::createFromPayload($payload);
    // UpdateController
    $payload = $this->createPayload($request->getContent(), UserUpdatePayload::class);
    $user->applyUpdatePayload($payload);
    A la place, payloads + entités riches

    View Slide

  35. 3.
    Communiquer sur le
    changement
    Versionner et déprécier

    View Slide

  36. 36
    Une fois qu’on a découplé l’interne de l’externe,
    on peut adopter des processus de gestion du
    changement

    View Slide

  37. 37
    API Semantic Versionning
    Versionner l’input/output de l’API

    View Slide

  38. 38
    Techniques de versionning
    Host :
    https://v1-1.api.selency.com/users
    Path prefix :
    https://api.selency.com/v1.1/users
    Header :
    Accept: application/vnd.selency.v1.1+json

    View Slide

  39. 39
    Majeure . Mineure . Patch
    Incompatibilité
    I/O
    Bugfix compatible
    I/O
    Feature compatible
    I/O

    View Slide

  40. 40
    Compatibilité en input
    Ajouter un champ requis ❌
    Ajouter un champ optionnel ✅
    Renommer un champ ❌
    Supprimer (ignorer) un champ ✅
    Compatibilité en output
    Ajouter un champ ✅
    Renommer un champ ❌
    Supprimer un champ ❌

    View Slide

  41. 41
    Gérer plusieurs versions d’API
    dans le même projet
    => un transformer/DTO par version majeure

    View Slide

  42. 42
    class V1\BookTransformer
    {
    public function transform(Book $book)
    {
    return [
    'id' => $book->getId(),
    'title' => $book->getTitle(),
    'year' => $book->getYear(),
    ];
    }
    }
    class V2\BookTransformer
    {
    public function transform(Book $book)
    {
    return [
    'id' => $book->getId(),
    'title' => $book->getTitle(),
    'date' => $book->getDate(),
    ];
    }
    }

    View Slide

  43. 43
    class V1\AuthRegisterPayload
    {
    #[Assert\NotBlank]
    public $email;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $password;
    }
    class V2\AuthRegisterPayload
    {
    #[Assert\NotBlank]
    public $email;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $firstName;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $lastName;
    #[Assert\Type(type: 'string')]
    #[Assert\NotBlank]
    public $password;
    }

    View Slide

  44. 44
    Le versionning rend indépendant le calendrier
    des consommateurs de celui de l’API
    => stabilité, fiabilité, confiance

    View Slide

  45. 45
    Créer des chemins de migration
    grâce aux dépréciations

    View Slide

  46. 46
    Chemin de migration en input
    Ajouter/Renommer un champ requis
    1. Ajout du nouveau champ en optionnel
    2. Dépréciation du précédent champ (si applicable)
    3. Utilisation priorisée nouveau > ancien
    4. Prochaine majeure : suppression ancien champ

    View Slide

  47. 47
    Chemin de migration en output
    Renommer un champ
    1. Ajout du nouveau nom de champ (même valeur)
    2. Dépréciation du précédent nom
    3. Prochaine majeure : suppression ancien champ

    View Slide

  48. 48
    Chemin de migration en output
    Supprimer un champ
    1. Dépréciation
    2. Prochaine majeure : suppression ancien champ

    View Slide

  49. 4.
    Documenter pour
    expliquer
    Expliquer le pourquoi et le comment

    View Slide

  50. 50
    Versionner sans communiquer est inutile
    Il faut un moyen de communiquer aux
    consomateurs

    View Slide

  51. 51
    OpenAPI
    Standard de documentation API
    Gère très bien les dépréciations
    Standard => clients, viewers, …

    View Slide

  52. 52
    Selency OpenAPI
    Notre librairie de création de doc OpenApi :
    https://github.com/selency/openapi
    Permet d’associer la documentation aux
    transformers + payloads

    View Slide

  53. 53
    class AuthRegisterPayload implements SelfDescribingSchemaInterface
    {
    // ...
    public static function describeSchema($schema, $openApi): void
    {
    $schema
    ->required(['email', 'firstName', 'lastName', 'password'])
    ->property('email', $openApi->schema()
    ->type('string')
    ->example('[email protected]')
    )
    ->property('firstName', $openApi->schema()
    ->type('string')
    ->example('John')
    )
    ->property('lastName', $openApi->schema()
    ->type('string')
    ->example('Doe')
    )
    ->property('password', $openApi->schema()
    ->type('string')
    ->description('Plain text password')
    )
    ;
    }
    }

    View Slide

  54. 54
    class AuthRegisterPayload implements SelfDescribingSchemaInterface
    {
    // ...
    public static function describeSchema($schema, $openApi): void
    {
    $schema
    ->required(['email', 'firstName', 'lastName', 'password'])
    ->property('email', $openApi->schema()
    ->type('string')
    ->example('[email protected]')
    )
    ->property('firstName', $openApi->schema()
    ->type('string')
    ->example('John')
    )
    ->property('lastName', $openApi->schema()
    ->type('string')
    ->example('Doe')
    ->deprecated(true)
    )
    ->property('password', $openApi->schema()
    ->type('string')
    ->description('Plain text password')
    )
    ;
    }
    }

    View Slide

  55. 55
    // openapi/V2/Documentation.php
    $doc->path('/auth/register', $this->openApi->pathItem()
    ->post($this->openApi->apiOperation()
    ->tag('Auth')
    ->operationId('app.auth.register')
    ->summary('Register')
    ->description('Create a user, return a user token')
    ->securityRequirement(null)
    ->requestBody($this->openApi
    ->requestBody()
    ->required(true)
    ->content('application/json', AuthRegisterPayload::class)
    )
    ->responses($this->openApi->responses()
    ->response('200', $this->openApi->response()
    ->description('Return the token of the new user')
    ->content('application/json', AuthTokenTransformer::class)
    )
    )
    )
    );

    View Slide

  56. 56
    // openapi/V2/Documentation.php
    $doc->path('/auth/register', $this->openApi->pathItem()
    ->post($this->openApi->apiOperation()
    ->tag('Auth')
    ->operationId('app.auth.register')
    ->summary('Register')
    ->description('Create a user, return a user token')
    ->securityRequirement(null)
    ->requestBody($this->openApi
    ->requestBody()
    ->required(true)
    ->content('application/json', AuthRegisterPayload::class)
    )
    ->responses($this->openApi->responses()
    ->response('200', $this->openApi->response()
    ->description('Return the token of the new user')
    ->content('application/json', AuthTokenTransformer::class)
    )
    )
    )
    );

    View Slide

  57. Conclusion
    ● Déconnecter l’interne de l’externe
    ● Communiquer le changement
    ● Documenter pour expliquer
    57

    View Slide

  58. 58
    Merci !
    Des questions ?
    Retrouvez moi sur :
    ◦ Twitter @titouangalopin // GitHub @tgalopin
    [email protected]
    Design by slidescarnival.com

    View Slide