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

Concevoir son API pour le futur

Concevoir son API pour le futur

Avatar for Titouan Galopin

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