Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add new tutorial: API versioning et rétro-compatibilité avec Symfony
  • Loading branch information
eko committed Mar 15, 2018
1 parent e3e0349 commit a009bf6
Show file tree
Hide file tree
Showing 8 changed files with 605 additions and 1 deletion.
@@ -0,0 +1,34 @@
{
"title": "API versioning et rétro-compatibilité avec Symfony",
"slug": "api-versioning-et-retro-compatibilite-avec-symfony",
"permalink": "/fr/api-versioning-et-retro-compatibilite-avec-symfony/",
"excerpt": "Lorsque vous avez besoin de faire évoluer votre API rapidement, vous êtes souvent bloqués par vos clients pour qui vous ne pouvez pas casser la compatibilité. Ce tutoriel vous explique comment mettre en place du versioning dans Symfony et comment gérer la rétro-compatibilité des sorties d'API.",
"stepTitles": [
"Introduction",
"Configuration des fichiers de changement par version",
"Ajout du listener sur la réponse Symfony",
"Ajout de la factory pour instancier les changements",
"Ajout de fichiers de changements",
"Conclusion"
],
"time": 15,
"date": "2018-03-04",
"cover": "/assets/2018-03-04-api-versioning-et-retro-compatibilite-avec-symfony/cover.jpg",
"authors": [
{
"name": "Vincent Composieux",
"username": "vcomposieux"
}
],
"categories": [
"Symfony",
"API"
],
"tags": [
"php",
"Symfony",
"api",
"versioning",
"compatibility"
]
}
@@ -0,0 +1,37 @@
Lorsque vous travaillez sur une API (principalement REST) et que vous souhaitez sortir de nouvelles fonctionnalités, une problématique montre souvent le bout de son nez : vous ne pouvez pas mettre en production avant que les applications clientes de votre application soient compatibles avec ces changements.

Afin de pouvoir livrer rapidement de nouvelles fonctionnalités ou encore des modifications au niveau du schéma de données, il vous faut alors mettre en place du versioning sur votre API.

Malheureusement, les manières de traiter réellement le sujet sont assez floues aujourd'hui.

J'ai alors parcouru différentes solutions et j'ai choisi d'adopter [le modèle présenté par Stripe](https://stripe.com/blog/api-versioning), permettant d'appliquer une rétro-compatibilité du modèle de données pour les versions précédentes.

# Objectif

Pour la suite de ce tutoriel Codelabs, nous allons imaginer que nous développons une API et que nous souhaitons sortir une nouvelle version `1.2.0` en production qui incluera des changements au niveau de notre modèle de données par rapport aux versions précédentes.

L'objectif est alors de sortir en production notre nouvelle version, et que chaque client qui appelle notre API sans spécifier de version particulière puisse bénéficier de cette nouvelle version.

En revanche, si un client, lors de sa requête, spécifie une version (comme `1.1.0` par exemple), alors il doit continuer à récupérer le même modèle de données que précédemment.

D'un point de vu technique, notre API devra alors appliquer une transformation sur le modèle de sortie afin d'assurer la rétro-compatibilité sur cette version. C'est vraiment la réponse de notre API qui sera versionnée.

# Gestion du numéro de version

Pour la suite de cet article, j'ai choisi de partir sur un numéro de version spécifié en header de requête, type `X-Accept-Version: 1.1.0`.

À vous de choisir ce qui vous conviendra le mieux mais je trouve la solution du header plus simple à maintenir et surtout, lorsque vous décidez de ne plus supporter une version, cela n'a pas d'impact sur les endpoints d'appel à votre API, vous renvoyez simplement la dernière version de votre API.

# Pré-requis

Avant de débuter l'implémentation technique, il vous faut disposer d'une instance Symfony. Vous pouvez vous rendre sur [http://symfony.com/download](http://symfony.com/download) pour installer une version de Symfony.

Cet article n'est pas spécifique à Symfony 4, cependant, si vous souhaitez installer cette dernière version, vous pouvez directement utiliser composer :

```
$ composer create-project symfony/skeleton api-versioning
```

# Prochaine étape

Une fois la logique claire, nous pouvons commencer à implémenter la configuration des changements en fonction du numéro de version dans notre application Symfony.
@@ -0,0 +1,30 @@
Nous allons donc avoir besoin de spécifier les changements de rétro-compatibilité à appliquer lorsqu'une version précédente est demandée.

Il nous faut alors implémenter une liste des versions dans la configuration de Symfony avec, pour chaque version, le namespace complet du fichier qui contient les versions à appliquer.

# Spécifions les versions rétro-compatibles

Editez alors le fichier `app/config/parameters.yml` de votre projet (ou `config/services.yaml` sous Symfony 4) et ajoutez-y l'entrée suivante sous `parameters` :

```yaml
parameters:
versions:
1.1.0: Acme\VersionChanges\VersionChanges110
1.0.0: Acme\VersionChanges\VersionChanges100
0.9.0: Acme\VersionChanges\VersionChanges009
0.8.0: Acme\VersionChanges\VersionChanges008
```

Nous spécifions une liste de la version la plus récente à la plus ancienne.

> **Note** : La version actuelle (1.2.0) n'apparaît pas dans cette liste car il s'agit ici uniquement de la liste des versions sur lesquels nous souhaitons appliquer une rétro-compatibilité.
Les changements de rétro-compatibilité seront alors appliqués dans ce même ordre.

Ainsi, dans le cas ou un client ajoute un header `X-Accept-Version: 0.9.0` dans ses requêtes, alors, les changements de rétro-compatibilité des versions seront joués respectivement dans l'ordre `1.1.0`, `1.0.0` puis `0.9.0`.

La version `0.8.0` ne devra quand à elle ne pas être jouée car elle correspond à un modèle encore plus ancien que celui demandé.

# Prochaine étape

Cette configuration doit ensuite être interprêtée par Symfony et les changements nécessaires appliqués à la réponse de votre API en fonction de la version demandée.
@@ -0,0 +1,134 @@
Nous allons donc agir sur la réponse de Symfony, ce qui signifie que nous devons implémenter un listener sur l'événement `kernel.response` du framework.

# Ajout de la classe

Commençons donc par créer la classe PHP du listener. Créez un ficier `Acme\Event\Listener\VersionChangesListener` :

```php
<?php

namespace Acme\Event\Listener;

use Acme\VersionChanges\ChangesFactory;

use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\RequestStack;

class VersionChangesListener
{
/**
* @var RequestStack
*/
protected $requestStack;

/**
* @var ChangesFactory
*/
protected $changesFactory;

/**
* Constructor.
*
* @param RequestStack $requestStack
* @param ChangesFactory $changesFactory
*/
public function __construct(RequestStack $requestStack, ChangesFactory $changesFactory)
{
$this->requestStack = $requestStack;
$this->changesFactory = $changesFactory;
}

/**
* @return Request
*/
private function getRequest()
{
return $this->requestStack->getCurrentRequest();
}
}
```

La structure du listener est en place. Comme vous pouvez le voir, nous y injectons le service `RequestStack` de Symfony ainsi qu'un service nommé `ChangesFactory`. Nous allons créer ce service dans les étapes juste après.

Le service `RequestStack` va nous servir à récupérer le numéro de version demandé en header de la requête et `ChangesFactory` s'occupera de nous instancier et de nous retourner les classes de changements de rétro-compatibilité de notre API, par la suite.

Ajoutons donc la méthode `onKernelResponse` qui sera déclenché par l'`EventManager` de Symfony :

```php
/**
* @param FilterResponseEvent $event
*/
public function onKernelResponse(FilterResponseEvent $event)
{
$version = $this->getRequest()->headers->get('X-Accept-Version');

$versionChanges = $this->changesFactory->getHistory($version);

if (!$versionChanges) {
return;
}

$data = json_decode($event->getResponse()->getContent(), true);
$data = $this->apply($versionChanges, $data);

$response = $event->getResponse();
$response->setContent(json_encode($data));

$event->setResponse($response);
}
```

Nous récupérons ici la valeur de la version envoyée dans le header `X-Accept-Version`, demandons au service `ChangesFactory` de nous récupérer l'historique des changements à jouer pour cette version, puis, si nous en trouvons, nous récupérons les données de la version actuelle et appelons une méthode `apply($versionChanges, $data)` que nous allons déclarer dès maintenant afin de jouer les changements.

Les nouvelles données seront ensuite mises à jour dans l'object réponse de Symfony et envoyées au client.

Pour jouer les changements, il nous manque donc la méthode `apply()` :

```php
/**
* Apply given version changes for given data.
*
* @param array $versionChanges
* @param array $data
*
* @return array
*/
private function apply($versionChanges, $data)
{
foreach ($versionChanges as $version => $changes) {
if (!$changes->supports($data)) {
continue;
}

$data = $changes->apply($data);
}

return $data;
}
```

On commence à deviner l'interface qui sera implémentée par les fichiers d'application de changements par la suite. Un premier appel à la méthode `supports()` permet de vérifier si les changements de ce fichier doivent être appliqués à cette version.

En effet, dans certains cas (certains endpoints d'API), les données renvoyées ne seront jamais impactées par ces changements. Cette méthode permet de s'assurer que les changements doivent bien être appliqués.

Enfin, `$changes->apply()` joue les changements nécessaires.

# Ajout du service Symfony

Afin que notre listener soit effectif, il ne nous reste plus qu'à déclarer le service dans l'injection de dépendance du framework :

```yaml
acme.event.version_changes_listener:
class: Acme\Event\Listener\VersionChangesListener
arguments: ["@request_stack", "@acme.version.changes_factory"]
tags:
- { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }
```

Le service `acme.version.changes_factory` est manquant à ce niveau, cela est normal, il sera déclaré dans la prochaine étape.

# Prochaine étape

Entrons dans le coeur du gestionnaire de changements de rétro-compatibilité en implémentant le service `ChangesFactory` qui nous permet d'instancier les classes de changements.

0 comments on commit a009bf6

Please sign in to comment.