Panorama rapide
Symfony 7.3 introduit deux composants phares :
- ObjectMapper (expérimental), développé par Antoine Bluchet, qui vise à transformer un objet en un autre de manière déclarative, principalement au moyen d’attributs PHP.
- JsonPath (expérimental), qui apporte à PHP une implémentation du standard JSONPath (RFC 9535, publiée en février 2024), avec un crawler JSON et un builder pour formuler des requêtes lisibles et testables, ainsi qu’un trait d’assertions PHPUnit dédié.
Les deux composants sont expérimentaux : leurs API publiques peuvent évoluer jusqu’à leur stabilisation (objectif évoqué : Symfony 7.4/8.0, fin novembre 2025 pour JsonPath).
ObjectMapper : la transformation d’objets, enfin native côté Symfony
Pourquoi un composant de mapping ?
Dans la vie d’une application, il est courant de transformer des données d’une structure à une autre :
- convertir des DTO en Entités (ou l’inverse) ;
- adapter des données d’API tierces à un modèle de domaine interne ;
- isoler du code legacy derrière des façades plus modernes.
ObjectMapper répond à ce besoin omniprésent avec une approche déclarative (attributs PHP), pour améliorer lisibilité et maintenabilité du code.
Principe et configuration par attributs
Le composant s’appuie sur l’attribut #[Map]
pour décrire les règles de transformation : cibles, renommages, conditions, transformations.
// src/Dto/ProductInputDto.php
namespace App\\Dto;
use App\\Entity\\Product;
use Symfony\\Component\\ObjectMapper\\Attribute\\Map;
#[Map(target: Product::class)]
class ProductInputDto
{
#[Map(target: 'name')]
public string $productName;
public string $description;
#[Map(if: false)]
public string $internalSku = '';
#[Map(transform: 'floatval')]
public string $price;
}
Côté entité :
// src/Entity/Product.php
namespace App\\Entity;
use Doctrine\\ORM\\Mapping as ORM;
#[ORM\\Entity]
class Product
{
#[ORM\\Id]
#[ORM\\GeneratedValue]
#[ORM\\Column]
private ?int $id = null;
#[ORM\\Column]
public string $name;
#[ORM\\Column]
public string $description;
#[ORM\\Column]
public float $price;
#[ORM\\Column]
public \\DateTimeImmutable $createdAt;
public function __construct()
{
$this->createdAt = new \\DateTimeImmutable();
}
public function getId(): ?int
{
return $this->id;
}
}
Points notables issus des sources :
#[Map(target: ...)]
sur la classe : cible de transformation par défaut.#[Map(target: '...')]
sur une propriété : renommage (ex.productName
→name
).#[Map(if: false)]
: exclusion conditionnelle d’un champ.#[Map(transform: 'floatval')]
: transformation via un callable (ou un service, les deux sont possibles).- Les autres propriétés sont mappées par nom identique si elles existent dans la cible.
Utilisation dans un contrôleur
Exemple d’intégration avec ObjectMapperInterface
et #[MapRequestPayload]
:
// src/Controller/ProductController.php
namespace App\\Controller;
use App\\Dto\\ProductInputDto;
use App\\Entity\\Product;
use Doctrine\\ORM\\EntityManagerInterface;
use Symfony\\Component\\HttpKernel\\Attribute\\AsController;
use Symfony\\Component\\HttpFoundation\\Response;
use Symfony\\Component\\HttpKernel\\Attribute\\MapRequestPayload;
use Symfony\\Component\\ObjectMapper\\ObjectMapperInterface;
use Symfony\\Component\\Routing\\Annotation\\Route;
#[AsController]
class ProductController
{
#[Route('/product', name: 'product_create', methods: ['POST'])]
public function create(
#[MapRequestPayload] ProductInputDto $productInput,
ObjectMapperInterface $objectMapper,
EntityManagerInterface $entityManager
): Response {
// 2e argument optionnel : nom de classe cible ou objet cible
$product = $objectMapper->map($productInput);
$entityManager->persist($product);
$entityManager->flush();
return $this->json(
['message' => 'Product created successfully: ' . $product->name],
Response::HTTP_CREATED
);
}
}
Statut et perspectives
- Statut : expérimental (API susceptible d’évoluer).
- Discussions : ouvertes sur GitHub (discussion #54476 mentionnée).
- Pistes d’évolution évoquées : inspiration d’outils existants (ex. AutoMapper), génération de code pour les perfs, services de transformation réutilisables (ex. TargetClass, MapCollection), et intégrations plus fines avec d’autres composants ; travail en cours pour API Platform.
JsonPath : le standard JSONPath arrive dans Symfony (et PHP)
Contexte standard
- Une spécification officielle JSONPath a été publiée en février 2024 (RFC 9535).
- De nombreux langages l’ont déjà adoptée (nativement ou via bibliothèques).
- Symfony 7.3 introduit un composant JsonPath pour PHP.
Installation
composer require symfony/json-path
Syntaxe JSONPath
$
: racine du document..
/[]
: accès aux enfants (objet / tableau)...
: descente récursive.*
: joker (tous les enfants).- Filtres :
?(@.price < 10)
avec opérateurs de comparaison. - Fonctions built-in prises en charge :
length
,count
,match
,search
,value
. - Slicing façon Python :
[0]
,[-1]
,[2:-2]
,[-3:]
,[::2]
,[0:9:3]
(début, fin, pas).
Les fonctions personnalisées sont prévues par la RFC et pourraient arriver dans le composant (non présentes « à l’heure actuelle »)
Le JsonCrawler : requêter efficacement
Exemple de données :
$json = <<<'JSON'
{"store": {"book": [
{"category": "reference", "author": "Nigel Rees", "title": "Sayings", "price": 8.95},
{"category": "fiction", "author": "Evelyn Waugh", "title": "Sword", "price": 12.99}
]}}
JSON;
Requêtes simples et filtrées :
use Symfony\\Component\\JsonPath\\JsonCrawler;
$crawler = new JsonCrawler($json);
$result = $crawler->find('$.store.book[0].title'); // ['Sayings']
$result = $crawler->find('$.store.book[?(@.price < 10)]'); // 1er livre
$result = $crawler->find('$.store.book[?length(@.author) > 11)]'); // 2e livre
$result = $crawler->find('$.store.book[?match(@.author, "[A-Z].*el.+")]'); // tous les livres
Traitement de gros volumes : le crawler accepte aussi des ressources. Avec le composant JsonStream (dépendance optionnelle), un mécanisme interne devine la plus petite sous-chaîne à décoder selon le chemin, ce qui économise mémoire et CPU.
Le builder JsonPath
: écrire des chemins de façon fluide
Un builder orienté objet permet d’exprimer un path de manière programmatique :
use Symfony\\Component\\JsonPath\\JsonPath;
$path = (new JsonPath())
->key('store')
->key('book')
->slice(start: 1, end: 5, step: 2)
->filter('match(@.author, "[A-Z].*el.+")');
$result = $crawler->find($path);
Fonctionnalités identiques à l’écriture sous forme de chaîne ; il s’agit d’une préférence de style.
Assertions PHPUnit intégrées
Le composant fournit un trait d’assertions pour faciliter les tests (DX). Exemples :
use PHPUnit\\Framework\\TestCase;
use Symfony\\Component\\JsonPath\\Test\\JsonPathAssertionsTrait;
class MyApiTest extends TestCase
{
use JsonPathAssertionsTrait;
public function testItFetchesAllUsers(): void
{
$json = self::fetchUserCollection();
$this->assertJsonPathCount(3, '$.users[*]', $json);
$this->assertJsonPathSame(['Melchior'], '$.users[0].username', $json);
$this->assertJsonPathEquals(['30'], '$.users[0].age', $json, 'should return the age as string or int');
}
public function testItFetchesOneUser(): void
{
$json = self::fetchOneUser();
$this->assertJsonPathSame(['Melchior'], '$.user.username', $json);
$this->assertJsonPathEquals(['30'], '$.user.age', $json, 'should return the age as string or int');
}
}
Statut et feuille de route
- Statut : expérimental (API sujette à modification, non couverte par la promesse de rétrocompatibilité).
- À venir : fonctions personnalisées, intégration au FrameworkBundle.
- Stabilisation visée : Symfony 7.4/8.0, fin novembre 2025 (si tout se passe bien).
Conseils pratiques
- ObjectMapper : privilégier une configuration déclarative via
#[Map]
pour documenter clairement la transformation ; utiliser des transformations simples (callables) ou des services quand la logique le nécessite ; envisager le mapping par défaut (propriétés homonymes). - JsonPath : pour des payloads volumineux, envisager l’usage de ressources et du composant JsonStream afin de limiter le décodage à la plus petite portion utile ; tirer parti des filtres et fonctions built-in pour écrire des requêtes expressives ; adopter le builder si vous préférez une syntaxe orientée objet ; utiliser les assertions PHPUnit pour des tests d’API lisibles et robustes.
Conclusion
Symfony 7.3 marque une étape importante autour de la manipulation de données :
- avec ObjectMapper, la transformation d’objets gagne une solution dédiée et déclarative, pensée pour les usages récurrents (DTO ↔︎ Entités, intégration d’APIs tierces, encapsulation de legacy) et déjà exploitable dans un flux HTTP via
#[MapRequestPayload]
etObjectMapperInterface
; - avec JsonPath, Symfony apporte à PHP une implémentation du standard JSONPath, adaptée aux gros volumes (via JsonStream), testable (assertions PHPUnit), et agréable à utiliser (builder).
Les deux composants sont expérimentaux : ils évolueront encore d’ici leur stabilisation. Pour autant, ils sont utilisables dès maintenant et offrent des gains concrets en clarté, performance et testabilité dans les projets qui manipulent intensivement des données.
Ecrit par
Alyson Paya
Partager l'article :
Un site vitrine ? e-commerce ? une application ?