Personnaliser les données renvoyées
Quand on renvoie des données au client, il peut être utile d'ajouter des informations supplémentaires.
Par exemple, quand on envoie nos articles au format JSON, on aimerait ajouter un lien vers l'article. En effet, les services externes qui veulent nos articles veulent également créer un lien vers l'article original, sur notre site.
Au lieu de laisser les autres services générer un lien, nous allons intégrer directement un lien complet vers l'article en question, dans le JSON renvoyé.
La normalisation
Nous avons vu que le serializer de Symfony exécute d'abord une étape de normalisation, pour passer des données au format tableau (array), avant de passer à la seconde étape dite d'encodage, pour transformer la donnée dans le type cible (JSON, XML, CSV, etc...).

Nous allons donc créer un Normalizer personnalisé, qui viendra définir la manière dont un article doit être représenté sous forme de tableau.
Créer un normalizer
Avec le maker, nous pouvons créer un normalizer :
php bin/console make:serializer:normalizer
Si nous le nommons ArticleNormalizer, alors une nouvelle classe va être créée par le maker dans le dossier src/Serializer :
<?php
namespace App\Serializer\Normalizer;
use App\Entity\Article;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class ArticleNormalizer implements NormalizerInterface
{
public function __construct(
// L'attribut Autowire permet d'injecter le service chargé de la normalisation d'objets
// Dont l'alias est ici : 'serializer.normalizer.object'
#[Autowire(service: 'serializer.normalizer.object')]
private NormalizerInterface $normalizer,
)
{
}
public function normalize($object, ?string $format = null, array $context = []): array
{
$data = $this->normalizer->normalize($object, $format, $context);
// TODO: add, edit, or delete some data
return $data;
}
public function supportsNormalization($data, string $format = null, array $context = []): bool
{
return $data instanceof Article;
}
/**
* Méthode présente pour la performance :
* permet à Symfony de retenir que ce normalizer peut être appelé pour les articles.
* La valeur true permet de mettre le résultat de 'supportsNormalization' en cache,
* pour une performance accrue.
*/
public function getSupportedTypes(?string $format): array
{
return [Article::class => true];
}
}
Structure du Normalizer
Notre Normalizer, par défaut, implémente 1 interface : NormalizerInterface. C'est elle qui impose l'implémentation des 2 méthodes supportsNormalization et normalize.
Nous l'avons déjà aperçu quand nous avons utilisés les Custom Voters, ou encore les Event Subscribers : l'autoconfiguration détecte une interface que votre classe implémente et lui ajoute automatiquement un tag.
C'est ainsi qu'un custom voter sera automatiquement tagué security.voter, par exemple.
Ici, notre normalizer est automatiquement tagué serializer.normalizer.
Vous pourrez le retrouver dans la sortie de la commande suivante :
php bin/console debug:container --tag serializer.normalizer
Ainsi, Symfony enregistre tous les normalizers présents dans l'application (y compris ceux inclus par défaut dans le serializer), et quand il a besoin de normaliser une données, il va les interroger en appelant la méthode supportsNormalization.
Notre normalizer, lui, va répondre qu'il supporte la normalisation seulement si la donnée ($data) est une instance de la classe Article. C'est ce qu'on souhaite, puisqu'on veut que notre normalizer s'occupe des articles.
Quand notre normalizer va répondre qu'il peut s'occuper des articles, alors Symfony va exécuter sa méthode normalize, pour l'exécuter.
Ajouter notre donnée
Quand nous voulons normaliser, avons-nous vraiment envie d'énumérer tous les champs de l'article, juste pour y rajouter son URL ?
Est-ce qu'on ne préfèrerait pas exécuter la normalisation initialement prévue par Symfony (celle qu'on a actuellement), pour venir simplement y rajouter notre donnée ensuite ?
C'est précisément ce qui est prévu dans la classe que le maker nous a créée :
public function __construct(
#[Autowire(service: 'serializer.normalizer.object')]
private NormalizerInterface $normalizer,
) {
}
public function normalize($object, ?string $format = null, array $context = []): array
{
$data = $this->normalizer->normalize($object, $format, $context);
// TODO: add, edit, or delete some data
return $data;
}
Une instance d'ObjectNormalizer est automatiquement injectée à la construction de notre normalizer.
Cet ObjectNormalizer est en réalité celui qui est utilisé pour transformer nos articles en tableaux : c'est la représentation qu'on voit actuellement du côté client.
Dans notre méthode normalize, la logique est donc très simple : nous allons d'abord demander au normalizer initial de normaliser l'article, puis, une fois que nous aurons la donnée (le tableau $data), alors nous le personnaliserons comme on le voudra.
Rappelons-nous : nous voulons ajouter une URL vers notre article. Pour ce faire, nous avons besoin du routeur à travers la UrlGeneratorInterface. Nous allons donc l'injecter dans notre normalizer, puis ajouter l'URL vers notre article à notre $data :
<?php
namespace App\Serializer\Normalizer;
use App\Entity\Article;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Serializer\Normalizer\CacheableSupportsMethodInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Serializer\Normalizer\ObjectNormalizer;
class ArticleNormalizer implements NormalizerInterface
{
public function __construct(
#[Autowire(service: 'serializer.normalizer.object')]
private NormalizerInterface $normalizer,
private UrlGeneratorInterface $router
) {
}
public function normalize($object, ?string $format = null, array $context = []): array
{
$data = $this->normalizer->normalize($object, $format, $context);
if (!$object instanceof Article) {
return $data;
}
$data['url'] = $this->router->generate(
'app_article_item',
['id' => $object->getId()],
UrlGeneratorInterface::ABSOLUTE_URL
);
return $data;
}
public function supportsNormalization($data, ?string $format = null, array $context = []): bool
{
return $data instanceof Article;
}
public function getSupportedTypes(?string $format): array
{
return [Article::class => true];
}
}
Nous venons donc rajouter l'URL comme donnée personnalisée dans la représentation d'un article renvoyée par notre API.
Dans Postman, on aura quelque chose comme ça :
[
{
"id": 680,
"title": "As there seemed to think to herself, and once.",
"date_created": "01/03/2022",
"content": "Alice, 'it's very easy to know what to ...",
"category": {
"id": 73,
"name": "necessitatibus"
},
"url": "http://localhost:8000/article/show/680"
},
{
"id": 681,
"title": "Gryphon: and Alice was thoroughly puzzled. 'Does.",
"date_created": "27/02/2022",
"content": "For instance, if you ...",
"category": {
"id": 74,
"name": "ut"
},
"url": "http://localhost:8000/article/show/681"
}
//...
]