Symfony 7.3 : deux nouveaux composants majeurs, ObjectMapper et JsonPath

17 septembre 2025 5 min
Avec Symfony 7.3, deux nouveaux composants font leur entrée : ObjectMapper, qui facilite la transformation d’objets grâce aux attributs PHP, et JsonPath, qui apporte enfin au PHP le standard officiel pour interroger efficacement des données JSON. Tous deux expérimentaux, ils ouvrent déjà des perspectives intéressantes en termes de clarté, testabilité et performance.

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. productNamename).
  • #[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] et ObjectMapperInterface ;
  • 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.

Merci de votre lecture 😎

Ecrit par
Alyson Paya

Partager l'article :

Vous avez un projet ?

Un site vitrine ? e-commerce ? une application ?

Contactez-nous

Découvrez les derniers articles

Tout voir