Symfony Serializer : guide complet avec exemples
Le composant Serializer de Symfony est un outil puissant qui permet de convertir des objets PHP en differents formats (JSON, XML, CSV) et inversement. Dans ce guide complet, je vous montre comment l'exploiter pleinement avec Symfony 7.2 et PHP 8.3 - des bases aux techniques avancees comme les normalizers custom et la gestion des references circulaires.
Comprendre l'architecture du Serializer
Le Serializer de Symfony repose sur un principe simple mais elegant : la separation entre normalisation et encodage. La normalisation transforme un objet en tableau PHP, et l'encodage convertit ce tableau en format de sortie. Cette separation permet une grande flexibilite.
// Le flux de serialisation
Objet PHP -> Normalizer -> Tableau PHP -> Encoder -> JSON/XML/CSV
// Le flux de deserialisation
JSON/XML/CSV -> Decoder -> Tableau PHP -> Denormalizer -> Objet PHP
Symfony embarque plusieurs normalizers par defaut, chacun specialise pour un type de donnee : ObjectNormalizer pour les objets generiques, DateTimeNormalizer pour les dates, ArrayDenormalizer pour les collections, et bien d'autres. Ils sont tries par priorite et le premier capable de traiter la donnee est utilise.
Installation et configuration
Si vous utilisez Symfony Flex (ce qui est le cas par defaut depuis Symfony 4), le composant est probablement deja installe. Sinon, ajoutez-le via Composer :
composer require symfony/serializer
# Pour le support des annotations/attributs PHP 8 :
composer require symfony/property-access symfony/property-info
Dans une application Symfony standard, le Serializer est configure automatiquement comme service. Vous pouvez l'injecter directement dans vos controllers ou services :
<?php
namespace App\Controller;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;
class ProductController
{
#[Route('/api/products/{id}', methods: ['GET'])]
public function show(
Product $product,
SerializerInterface $serializer
): JsonResponse {
$json = $serializer->serialize($product, 'json', [
'groups' => ['product:read'],
]);
return new JsonResponse($json, 200, [], true);
}
}
Les groupes de serialisation
Les groupes sont essentiels pour controler quelles proprietes sont exposees dans quel contexte. Avec PHP 8.3, on utilise les attributs natifs :
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Serializer\Attribute\Groups;
#[ORM\Entity]
class Product
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
#[Groups(['product:read', 'product:list'])]
private ?int $id = null;
#[ORM\Column(length: 255)]
#[Groups(['product:read', 'product:list', 'product:write'])]
private string $name;
#[ORM\Column(type: 'text')]
#[Groups(['product:read', 'product:write'])]
private string $description;
#[ORM\Column]
#[Groups(['product:read', 'product:list'])]
private float $price;
#[ORM\Column(type: 'datetime_immutable')]
#[Groups(['product:read'])]
private \DateTimeImmutable $createdAt;
// Getters et setters...
}
En utilisant le groupe product:list, seuls l'id, le nom et le prix seront serialises - ideal pour un endpoint de listing. Le groupe product:read inclura aussi la description et la date de creation pour la vue detail.
Gestion des references circulaires
Les relations bidirectionnelles entre entites Doctrine sont une source classique de problemes de serialisation. Un produit reference une categorie, qui reference ses produits, qui referent leur categorie - boucle infinie. Symfony offre plusieurs solutions :
// Solution 1 : circular_reference_handler
$json = $serializer->serialize($product, 'json', [
'circular_reference_handler' => function (object $object): mixed {
return $object->getId();
},
]);
// Solution 2 : MaxDepth (recommande)
use Symfony\Component\Serializer\Attribute\MaxDepth;
class Product
{
#[MaxDepth(1)]
#[Groups(['product:read'])]
private Category $category;
}
// N'oubliez pas d'activer MaxDepth dans le contexte :
$json = $serializer->serialize($product, 'json', [
'groups' => ['product:read'],
'enable_max_depth' => true,
]);
La solution #[MaxDepth] est plus propre car elle est declarative et s'applique automatiquement. Pour un projet avec beaucoup de relations imbriquees, c'est l'approche que je recommande a mes clients. Si vous construisez une API complexe, pensez aussi a consulter notre guide sur Messenger pour gerer les traitements lourds de maniere asynchrone.
Serialiser des DTO plutot que des entites
Exposer directement des entites Doctrine dans vos API est une pratique risquee : modification accidentelle de donnees, couplage fort entre schema de base et API publique, fuite d'informations sensibles. Les DTO (Data Transfer Objects) resolvent ces problemes :
<?php
namespace App\Dto;
use Symfony\Component\Serializer\Attribute\Groups;
final readonly class ProductResponse
{
public function __construct(
#[Groups(['api'])]
public int $id,
#[Groups(['api'])]
public string $name,
#[Groups(['api'])]
public string $slug,
#[Groups(['api'])]
public float $priceHT,
#[Groups(['api'])]
public float $priceTTC,
#[Groups(['api'])]
public string $categoryName,
) {}
public static function fromEntity(Product $product): self
{
return new self(
id: $product->getId(),
name: $product->getName(),
slug: $product->getSlug(),
priceHT: $product->getPrice(),
priceTTC: $product->getPrice() * 1.20,
categoryName: $product->getCategory()->getName(),
);
}
}
Avec cette approche, vous controlez exactement ce qui est expose, et vous pouvez ajouter des champs calcules (comme priceTTC) sans toucher a votre entite. La classe est readonly - impossible de la modifier apres construction, ce qui garantit l'integrite des donnees. Pour maitriser les formulaires associes, consultez notre guide ultime des FormType.
Formats de serialisation : JSON, XML, CSV
Le Serializer supporte nativement plusieurs formats de sortie. Le JSON est le plus courant pour les API, mais le XML reste incontournable pour certaines integrations B2B, et le CSV est pratique pour les exports :
// JSON (defaut pour les API REST)
$json = $serializer->serialize($products, 'json', [
'groups' => ['product:list'],
'json_encode_options' => JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE,
]);
// XML (integrations B2B, flux e-commerce)
$xml = $serializer->serialize($products, 'xml', [
'xml_root_node_name' => 'products',
'xml_encoding' => 'UTF-8',
]);
// CSV (exports, imports batch)
$csv = $serializer->serialize($products, 'csv', [
'csv_delimiter' => ';',
'csv_enclosure' => '"',
'csv_headers' => ['ID', 'Nom', 'Prix'],
]);
Ecrire un normalizer custom
Parfois les normalizers embarques ne suffisent pas. Vous devez peut-etre transformer des donnees de maniere specifique, integrer un service externe, ou gerer un format proprietaire. Voici comment creer un normalizer custom :
<?php
namespace App\Serializer\Normalizer;
use App\Entity\Product;
use App\Service\ImageUrlGenerator;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class ProductNormalizer implements NormalizerInterface
{
public function __construct(
#[Autowire(service: 'serializer.normalizer.object')]
private readonly NormalizerInterface $objectNormalizer,
private readonly ImageUrlGenerator $imageUrlGenerator,
) {}
public function normalize(
mixed $object,
?string $format = null,
array $context = []
): array {
/** @var Product $object */
$data = $this->objectNormalizer->normalize($object, $format, $context);
// Ajouter l'URL complete de l'image
if (isset($data['imagePath'])) {
$data['imageUrl'] = $this->imageUrlGenerator
->generate($data['imagePath']);
unset($data['imagePath']);
}
// Formater le prix avec devise
if (isset($data['price'])) {
$data['formattedPrice'] = number_format(
$data['price'], 2, ',', ' '
) . ' EUR';
}
return $data;
}
public function supportsNormalization(
mixed $data,
?string $format = null,
array $context = []
): bool {
return $data instanceof Product;
}
public function getSupportedTypes(?string $format): array
{
return [Product::class => true];
}
}
Le normalizer est automatiquement enregistre grace a l'autowiring. Attention toutefois a ne pas creer de dependance circulaire en injectant le SerializerInterface directement - utilisez le service serializer.normalizer.object comme montre ci-dessus.
Deserialisation et validation
La deserialisation est tout aussi importante que la serialisation. Combinez-la avec le composant Validator pour une validation robuste des donnees entrantes :
#[Route('/api/products', methods: ['POST'])]
public function create(
Request $request,
SerializerInterface $serializer,
ValidatorInterface $validator,
EntityManagerInterface $em
): JsonResponse {
try {
$dto = $serializer->deserialize(
$request->getContent(),
ProductCreateDto::class,
'json'
);
} catch (\Exception $e) {
return new JsonResponse(
['error' => 'Format JSON invalide'],
400
);
}
$errors = $validator->validate($dto);
if (count($errors) > 0) {
$messages = [];
foreach ($errors as $error) {
$messages[$error->getPropertyPath()] = $error->getMessage();
}
return new JsonResponse(['errors' => $messages], 422);
}
$product = Product::createFromDto($dto);
$em->persist($product);
$em->flush();
return new JsonResponse(
$serializer->serialize($product, 'json', ['groups' => ['product:read']]),
201,
[],
true
);
}
Pour approfondir les methodes de requete Doctrine utilisées dans vos repositories, jetez un oeil a notre reference findBy / findAll.
Bonnes pratiques et pieges a eviter
- Utilisez toujours des groupes - ne serialisez jamais un objet sans groupe, vous risquez d'exposer des champs sensibles (mots de passe hashés, tokens).
- Preferez les DTO aux entites - decouplage, securite et maintenabilite.
- Attention aux performances - le
ObjectNormalizerutilise la reflection PHP, ce qui peut etre lent sur de grandes collections. Activez le cache du PropertyInfo. - Testez votre serialisation - ecrivez des tests unitaires pour verifier le format de sortie de vos normalizers custom.
- Gerez les erreurs de deserialisation - ne faites jamais confiance aux donnees entrantes, wrappez toujours dans un try/catch.
Configuration avancee dans Symfony 7.2
Symfony 7.2 a introduit des ameliorations notables du composant Serializer. Le fichier config/packages/serializer.yaml vous permet de configurer le comportement global :
# config/packages/serializer.yaml
framework:
serializer:
enabled: true
default_context:
enable_max_depth: true
datetime_format: 'Y-m-d\TH:i:sP'
csv_delimiter: ';'
mapping:
paths:
- '%kernel.project_dir%/config/serializer/'
Vous pouvez aussi definir des contextes de serialisation nommes, reutilisables a travers votre application, en creant des fichiers YAML dans le dossier config/serializer/.
Conclusion
Le composant Serializer de Symfony est bien plus qu'un simple convertisseur JSON. C'est un systeme extensible qui gere la normalisation, les groupes, les references circulaires, les formats multiples et les transformations custom. Maitrisez-le et vos API seront robustes, securisees et maintenables.
Besoin d'aide pour developper une application web Symfony avec une API bien structuree ? Consultez nos tarifs ou contactez-moi pour discuter de votre projet.
