JMSSerializer : Une alternative au sérialiseur natif de Symfony

Le sérialiseur natif de Symfony est disponible depuis les toutes premières versions du framework. Cependant, les fonctionnalités supportées par celui-ci étaient assez basique.

Par exemple, les groupes de sérialisation - permettant entre autres de gérer les références circulaires - n’ont été supportés qu’à partir de la version 2.7 sortie en 2015. La sérialisation des dates PHP (DateTime et DateTimeImmutable) n’a été supporté qu’avec la version 3.1 sortie en 2016.

Pour pallier à ce retard, un bundle a été développé pour la gestion de la sérialisation dans Symfony : JMSSerializerBundle. Il permet d’intégrer la librairie JMSSerializer et est très largement utilisé dans le cadre du développement d’une API avec Symfony.

Pourquoi utiliser JMSSerializerBundle ?

Nous avons déjà eu l’occasion de voir le nom JMSSerializerBundle dans les premières parties de ce cours. Ce bundle permet d’inclure et de configurer la librairie PHP jms/serializer dans Symfony.

Cette librairie présente beaucoup d’avantages :

  • Elle est beaucoup plus mature que le sérialiseur de Symfony ;
  • De par son ancienneté, elle est supportée par beaucoup de bundles et facilite donc l’interopérabilité entre les bundles et/ou composants que nous pouvons utiliser dans notre API ;
  • Et pour finir, les ressources (documentation, cours etc.) sur cette librairie sont plus abondantes.

À l’installation de FOSRestBundle, nous étions obligés d’utiliser la version 2.0 afin de supporter pleinement le sérialiseur de Symfony. Mais avec JMSSerializerBundle, nous pourrons profiter de toutes les fonctionnalités de jms/serializer tout en utilisant une version de FOSRestBundle inférieure à la 2.0.

Installation et configuration de JMSSerializerBundle

Installation de JMSSerializerBundle

Comme pour tous les bundles de Symfony, il suffit de le télécharger avec Composer et de l’activer. Téléchargement du bundle :

1
2
3
composer require jms/serializer-bundle
# Using version ^1.1 for jms/serializer-bundle
./composer.json has been updated

Activation du bundle :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
<?php
# app/AppKernel.php
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Config\Loader\LoaderInterface;

class AppKernel extends Kernel
{
    public function registerBundles()
    {
        $bundles = [
            // ...
            new JMS\SerializerBundle\JMSSerializerBundle(),
            new FOS\RestBundle\FOSRestBundle(),
            new AppBundle\AppBundle(),
        ];
        // ...
        return $bundles;
    }
    // ...
}

Configuration de JMSSerializerBundle

La configuration par défaut de ce bundle suffit largement pour commencer à l’exploiter. Mais pour notre cas, puisque nous avons déjà pas mal de fonctionnalités qui dépendent du sérialiseur, nous allons modifier sa configuration.

Gestion des dates PHP

Comme pour le sérialiseur natif de Symfony (depuis la version 3.1), la sérialisation des dates dans php est supportée nativement par JMSSerializerBundle. Nous pouvons, en plus, personnaliser ce comportement avec juste 4 lignes de configuration.

1
2
3
4
5
6
7
8
# app/config/config.yml

# ...
jms_serializer:
    handlers:
        datetime:
            default_format: "Y-m-d\\TH:i:sP"
            default_timezone: "UTC"

La valeur "Y-m-d\\TH:i:sP" désigne le format ISO 8601 pour les dates.

L’attribut default_format prend en paramètre le même format que la fonction date de PHP.

Une question de casse : CamelCase ou snake_case ?

Dans tous les exemples que nous avons pu voir, les attributs dans les requêtes et les réponses sont toutes en minuscules. À part l’attribut plainPassword utilisé pour créer un utilisateur et le champ createdAt associé à un token d’authentification, toutes nos attributs sont en minuscule. Mais dans le cadre d’une API plus complète, la question de la casse va se poser.

La seule contrainte qu’il faudra garder en tête est la cohérence. Si nous décidons d’utiliser des noms d’attributs en camelCase ou en snake_case, il faudra s’en tenir à ça pour tous les appels de l’API.

La configuration de tels paramètres est très simple aussi bien avec le sérialiseur de base de Symfony qu’avec le JMSSerializer. Nous allons donc garder la configuration par défaut du sérialiseur de Symfony qui est de conserver le même nom que celui des attributs de nos objets.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# app/config/config.yml

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: services.yml }

parameters:
    locale: en
    jms_serializer.camel_case_naming_strategy.class: JMS\Serializer\Naming\IdenticalPropertyNamingStrategy

# ...

Désactivation du sérialiseur natif

Maintenant que nous avons fini la configuration, il faut désactiver le sérialiseur natif de Symfony.

Vu qu’il n’est pas activé par défaut, nous pouvons retirer la configuration associée ou passer sa valeur à false.

1
2
3
4
5
6
7
8
9
# app/config/config.yml

# ...

framework:
    # ...
    serializer:
        enabled: false
# ...

FOSRestBundle va maintenant utiliser directement le sérialiseur fournit par JMSSerializerBundle.

Sérialiser les attributs même s’ils sont nuls

Le comportement par défaut de JMSSerializer est d’ignorer tous les attributs nuls d’un objet. Ce fonctionnement peut entrainer des réponses avec des payloads partiels manquant certains attributs. Pour éviter ce problème, FOSRestBundle propose un paramètre de configuration pour forcer JMSSerializer à sérialiser les attributs nuls.

1
2
3
4
5
6
7
# app/config/config.yml

# ...

fos_rest:
    serializer:
        serialize_null:  true

Impact sur l'existant

Tests de la configuration

Pour tester notre configuration, nous allons lister les lieux dans notre application.

La réponse obtenue est :

1
2
3
4
{
  "0": {},
  "1": {}
}

Nous avons là une bonne et une mauvaise nouvelle. Le bundle est bien utilisé pour sérialiser la réponse mais les groupes de sérialisation, que nous avons définis, ne sont pas encore exploités.

Pourquoi la réponse n’est pas sérialisée correctement ?

Le sérialiseur est pleinement supporté par FOSRestBundle. Les configurations dans tous nos contrôleurs sont déjà compatibles. Par contre, le fichier src/AppBundle/Resources/config/serialization.yml décrivant les règles de sérialisation, est ignoré par JMSSerializerBundle.

La configuration par défaut se base sur une convention simple. Pour un bundle, les fichiers décrivant la sérialisation doivent être dans le dossier src/NomDuBundle/Resources/config/serializer/.

Le nom de chaque fichier contenant les règles de sérialisation d’une classe est obtenu en faisant deux opérations :

  • le nom du bundle est retiré du namespace (espace de nom) de la classe ;
  • les séparateurs anti-slash (\) sont remplacés par des points (.) ;
  • et enfin, l’extension yml ou xml est rajouté au nom ainsi obtenu.

Par exemple, pour la classe NomDuBundle\A\B, si nous voulons utiliser une configuration en YAML, nous devons avoir un fichier src/NomDuBundle/Resources/config/serializer/A.B.yml.

JMSSerialiserBundle supporte aussi les annotations et les fichiers XML pour la configuration des règles de sérialisation. D’ailleurs, si nous avions utilisé les annotations, le code fonctionnerait sans adaptation de notre part.

Mise à jour de nos règles de sérialisation

Pour remettre notre API d’aplomb, nous allons créer les fichiers de configuration pour les classes utilisées.

Commençons par l’entité Place. La configuration pour cette classe devient :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# src/AppBundle/Resources/config/serializer/Entity.Place.yml
AppBundle\Entity\Place:
    exclusion_policy: none
    properties:
        id:
            groups: ['place', 'price', 'theme']
        name:
            groups: ['place', 'price', 'theme']
        address:
            groups: ['place', 'price', 'theme']
        prices:
            groups: ['place']
        themes:
            groups: ['place']

Par défaut, aucune propriété de nos classes n’est affichée pendant la sérialisation. En mettant l’attribut exclusion_policy à none, nous configurons le sérialiseur pour inclure par défaut toutes les propriétés de la classe. Nous pourrons bien sûr exclure certaines propriétés à la demande (exclude: true).

De même, il est aussi possible d’adopter la stratégie inverse à savoir exclure par défaut toutes les propriétés de nos classes et les ajouter à la demande (expose: true).

Il faut aussi noter que l’attribut attributes dans l’ancien fichier de configuration est remplacé par properties. Tout le reste est identique à notre ancien fichier de configuration.

La configuration des nouvelles classes devient maintenant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# src/AppBundle/Resources/config/serializer/Entity.Price.yml
AppBundle\Entity\Price:
    exclusion_policy: none
    properties:
        id:
            groups: ['place', 'price']
        type:
            groups: ['place', 'price']
        value:
            groups: ['place', 'price']
        place:
            groups: ['price']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# src/AppBundle/Resources/config/serializer/Entity.Theme.yml
AppBundle\Entity\Theme:
    exclusion_policy: none
    properties:
        id:
            groups: ['place', 'theme']
        name:
            groups: ['place', 'theme']
        value:
            groups: ['place', 'theme']
        place:
            groups: ['theme']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# src/AppBundle/Resources/config/serializer/Entity.User.yml
AppBundle\Entity\User:
    exclusion_policy: none
    properties:
        id:
            groups: ['user', 'preference', 'auth-token']
        firstname:
            groups: ['user', 'preference', 'auth-token']
        lastname:
            groups: ['user', 'preference', 'auth-token']
        email:
            groups: ['user', 'preference', 'auth-token']
        preferences:
            groups: ['user']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# src/AppBundle/Resources/config/serializer/Entity.Preference.yml
AppBundle\Entity\Preference:
    exclusion_policy: none
    properties:
        id:
            groups: ['user', 'preference']
        name:
            groups: ['user', 'preference']
        value:
            groups: ['user', 'preference']
        user:
            groups: ['preference']
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# src/AppBundle/Resources/config/serializer/Entity.AuthToken.yml
AppBundle\Entity\AuthToken:
    exclusion_policy: none
    properties:
        id:
            groups: ['auth-token']
        value:
            groups: ['auth-token']
        createdAt:
            groups: ['auth-token']
        user:
            groups: ['auth-token']

`

N’oubliez pas de vider le cache pour éviter tout problème.

En testant cette nouvelle configuration, la liste des lieux dans notre application redevient correcte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
[
  {
    "id": 1,
    "name": "Tour Eiffel",
    "address": "5 Avenue Anatole France, 75007 Paris",
    "prices": [
      {
        "id": 1,
        "type": "less_than_12",
        "value": 5.75
      }
    ],
    "themes": [
      {
        "id": 1,
        "name": "architecture",
        "value": 7
      },
      {
        "id": 2,
        "name": "history",
        "value": 6
      }
    ]
  },
  {
    "id": 2,
    "name": "Mont-Saint-Michel",
    "address": "50170 Le Mont-Saint-Michel",
    "prices": [],
    "themes": [
      {
        "id": 3,
        "name": "history",
        "value": 3
      },
      {
        "id": 4,
        "name": "art",
        "value": 7
      }
    ]
  }
]

Vous pouvez tester l’ensemble des appels que nous avons déjà mis en place. L’API se comporte exactement de la même façon.


L’intégration de JMSSerializerBundle avec FOSRestBundle est aussi simple qu’avec le sérialiseur natif de Symfony. En effet, FOSRestBundle nous offre une interface unique et s’adapte au sérialiseur mis à sa disposition.

En plus, le bundle JMSSerializerBundle supporte beaucoup de fonctionnalités que nous n’avons pas abordées (gestion des versions, propriétés virtuelles, etc.).

Vous avez pu remarquer que JMSSerializer nécessite un peu plus de configuration que le sérialiseur natif de Symfony. Par contre, le travail fourni pour obtenir un résultat correct est très rapidement rentabilisé vu que JMSSerializerBundle s’intègre facilement avec beaucoup d’autres bundles de Symfony.

Nous aurons d’ailleurs l’occasion d’exploiter ce bundle dans le chapitre sur la documentation.