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. Agenda • De quoi parle-t-on ? • Déconnecter l’interne de

    l’externe • Communiquer le changement • Documenter pour expliquer 4
  2. 10 • Rester stable dans le temps • Créer des

    chemins de migration • Communiquer aux consommateurs
  3. 11 On a de la chance : Symfony a déjà

    créé ces processus ! Et si on les réutilisait ?
  4. 16 Transformer = Serializer manuel + Sous-ressources Exemple : League

    Fractal https://fractal.thephpleague.com (=> nous avons notre propre système à Selency)
  5. 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
  6. 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(), ] ], ]; } }
  7. 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(), ] ], ]; } }
  8. 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
  9. 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
  10. 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; }
  11. 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; }
  12. 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); }
  13. 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 ?
  14. 33 class User { public static function createFromPayload(AuthRegisterPayload $payload): self

    { // ... } public function applyUpdatePayload(UserUpdatePayload $payload) { // ... } } A la place, payloads + entités riches
  15. 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
  16. 36 Une fois qu’on a découplé l’interne de l’externe, on

    peut adopter des processus de gestion du changement
  17. 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
  18. 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 ❌
  19. 41 Gérer plusieurs versions d’API dans le même projet =>

    un transformer/DTO par version majeure
  20. 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(), ]; } }
  21. 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; }
  22. 44 Le versionning rend indépendant le calendrier des consommateurs de

    celui de l’API => stabilité, fiabilité, confiance
  23. 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
  24. 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
  25. 48 Chemin de migration en output Supprimer un champ 1.

    Dépréciation 2. Prochaine majeure : suppression ancien champ
  26. 51 OpenAPI Standard de documentation API Gère très bien les

    dépréciations Standard => clients, viewers, …
  27. 52 Selency OpenAPI Notre librairie de création de doc OpenApi

    : https://github.com/selency/openapi Permet d’associer la documentation aux transformers + payloads
  28. 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') ) ; } }
  29. 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') ) ; } }
  30. 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) ) ) ) );
  31. 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) ) ) ) );
  32. 58 Merci ! Des questions ? Retrouvez moi sur :

    ◦ Twitter @titouangalopin // GitHub @tgalopin ◦ [email protected] Design by slidescarnival.com