Slide 1

Slide 1 text

Concevoir son API pour le futur Inspirations de Symfony Titouan Galopin

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Introduction 3 Notre challenge à Selency

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

6 Une API est un contrat

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

8 Différentes personnes travaillent autour de ce contrat

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

14 Output Les transformers

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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(), ] ], ]; } }

Slide 20

Slide 20 text

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(), ] ], ]; } }

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

22 Input Utilisez des Data Transfer Object

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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; }

Slide 27

Slide 27 text

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; }

Slide 28

Slide 28 text

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); }

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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 ?

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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 ❌

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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(), ]; } }

Slide 43

Slide 43 text

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; }

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

4. Documenter pour expliquer Expliquer le pourquoi et le comment

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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') ) ; } }

Slide 54

Slide 54 text

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') ) ; } }

Slide 55

Slide 55 text

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) ) ) ) );

Slide 56

Slide 56 text

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) ) ) ) );

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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