Couverture de l'article JsonPath en PHP avec Symfony 7.3 : Pour des API Incassables, retour sur le talk d'Alexandre Daubois à l'AFUP DAY Lyon 2026
Retour aux articles

L'agence

WanadevStudio

JsonPath en PHP avec Symfony 7.3 : Pour des API Incassables, retour sur le talk d'Alexandre Daubois à l'AFUP DAY Lyon 2026

À l'AFUP Lyon 2026, Alexandre Daubois (CTO chez Les-Tilleuls.coop, membre des core teams Symfony et FrankenPHP, et mainteneur PHP) a présenté un nouveau composant Symfony qu'il a lui-même proposé et mergé : JsonPath. Le titre du talk résume bien l'ambition : en finir avec le code fragile qui navigue dans du JSON à coups de tableaux PHP imbriqués.

Sommaire


Le problème : naviguer dans du JSON en PHP, c'est verbeux et fragile

Le point de départ est un constat que tout développeur PHP a déjà vécu. Prendre un JSON représentant une librairie, des livres avec catégorie, auteur, titre et prix et en extraire les livres dont le prix est inférieur à 10€ demande en PHP classique :

$data = json_decode($json, true);
$results = [];

foreach ($data['store']['book'] as $book) {
    if (isset($book['price']) && $book['price'] < 10) {
        $results[] = $book;
    }
}

C'est verbeux, et surtout fragile : chaque accès à une clé inexistante peut faire exploser le code, les structures imbriquées génèrent des foreachs en cascade, et le moindre changement de structure JSON casse tout.

Alexandre rappelle que le monde XML avait résolu ce problème depuis longtemps avec XPath : DOMXPath::query('//book[price < 10]') fait exactement la même chose. La problématique est strictement identique pour JSON et pourtant, jusqu'en 2025, il n'existait pas d'équivalent sérieux en PHP.


Un standard qui a mis du temps à émerger : la RFC 9535

Depuis 1999, XPath a évolué jusqu'à sa version 3.1 (2017). Du côté JSON, le standard a attendu... 2024 pour être formellement spécifié dans la RFC 9535. Cette RFC décrit précisément la syntaxe JSONPath, une syntaxe qui existait de facto depuis des années, mais sans référence officielle, d'où les implémentations divergentes et peu nombreuses.

Le composant Symfony est l'une des premières implémentations 100% conformes à cette RFC (mergé dans Symfony 7.3 le 28 mars 2025).


La syntaxe JSONPath

JSONPath reprend les intuitions de XPath mais pour JSON. Le $ représente la racine du document :

$.store.book          // Tous les livres
$.store.bicycle.color // "red"
$.store.*             // Tout dans store (books + bicycle)

Le descendant récursif .. permet de chercher une clé à n'importe quelle profondeur :

$..author   // Tous les auteurs, peu importe la profondeur
$..price    // Tous les prix (livres + vélo = 5 résultats)

Les sélecteurs d'index et de slice permettent de naviguer dans les tableaux :

$..book[0]    // Premier livre
$..book[-1]   // Dernier livre
$..book[0,1]  // Les deux premiers
$..book[-2:]  // Les 2 derniers
$..book[::2]  // Un sur deux
$..book[::-1] // Tout, inversé

Les filtres : la vraie puissance de JSONPath s'expriment avec [?(...)] et l'opérateur @ pour référencer l'élément courant :

$..book[?(@.isbn)]                                      // Livres qui ONT un ISBN
$..book[?(@.price < 10)]                                // Livres à moins de 10€
$..book[?(@.price > 10 && @.category == 'fiction')]     // Tolkien uniquement
$..book[?(@.publisher.address.city == "Springfield")]   // Navigation imbriquée

Des fonctions sont également disponibles dans les expressions de filtre : length(), count(), match() (regex full match), search() (regex substring), value().


Le composant Symfony JsonPath

Disponible depuis Symfony 7.3, le composant s'installe avec :

composer require symfony/json-path

API DX-first : le JsonCrawler

L'entrée principale est JsonCrawler. Il accepte aussi bien une chaîne JSON qu'un stream (intégration possible avec JsonStreamer pour traiter de très gros fichiers sans tout charger en mémoire) :

$crawler = new JsonCrawler($json);
$results = $crawler->find('$.store.book[?(@.price < 10)]');

Performances : décodage partiel intelligent

L'un des points forts mis en avant est le comportement à l'évaluation. Pour une expression comme $.store.book[?(@.price < 10)], le composant est déterministe sur la partie $.store (navigation statique) et n'évalue le filtre qu'en décodant la sous-partie book. Pour $.data[0].name, l'expression est entièrement déterministe : seule la valeur name est décodée, crucial pour des JSON de grande taille.

Le Path Builder : construire des chemins programmatiquement

Plutôt que de manipuler des chaînes de caractères à la main (source d'erreurs), Symfony fournit la classe JsonPath avec une API fluide et immutable :

$books = (new JsonPath())->key('store')->key('book');

$first = $books->first();   // $.store.book[0]  — $books inchangé
$last  = $books->last();    // $.store.book[-1]
$all   = $books->all();     // $.store.book[*]

L'immutabilité est une propriété importante : chaque méthode retourne une nouvelle instance, on peut donc réutiliser une base commune sans effets de bord.

Pour les cas avancés, deepScan(), slice() et filter() complètent le builder :

(new JsonPath())->deepScan()->key('author'); // $..author

(new JsonPath())->key('items')->slice(0, 10, 2); // $.items[0:10:2]

Assertions PHPUnit : des API testables

Le composant embarque JsonPathAssertionsTrait, un trait à inclure dans ses tests PHPUnit pour exprimer des assertions directement sur du JSON :

use Symfony\Component\JsonPath\Test\JsonPathAssertionsTrait;

class BookstoreTest extends TestCase
{
    use JsonPathAssertionsTrait;

    public function testCheapBooks(): void
    {
        $json = file_get_contents('bookstore.json');

        $this->assertJsonPathCount(3, '$.store.book[?(@.price < 10)]', $json);
        $this->assertJsonPathEquals('Nigel Rees', '$.store.book[0].author', $json);
        $this->assertJsonPathContains('Moby Dick', '$..title', $json);
    }
}

Sept méthodes sont disponibles : assertJsonPathEquals, assertJsonPathNotEquals, assertJsonPathSame, assertJsonPathNotSame, assertJsonPathCount, assertJsonPathContains, assertJsonPathNotContains. C'est ici que réside la promesse des "API incassables" du titre : une réponse JSON peut désormais être vérifiée avec précision dans les tests fonctionnels, sans parcourir manuellement le tableau résultant de json_decode.

Extensibilité : créer ses propres fonctions

Le composant est extensible via l'attribut PHP #[AsJsonPathFunction]. Une classe invocable taguée ainsi devient disponible dans les expressions JSONPath :

#[AsJsonPathFunction('upper')]
final class UppercaseFunction
{
    public function __invoke(mixed $value): ?string
    {
        return is_string($value) ? strtoupper($value) : null;
    }
}

// Usage : $.items[?upper(@.title) == "HELLO"]

Les fonctions custom déclarent leur type de retour via l'enum FunctionReturnType : Value (pour des comparaisons), Logical (pour des prédicats booléens, comme un is_positive()), ou Nodes (pour retourner des listes de nœuds).


Un point de vigilance : le ReDos

Alexandre aborde honnêtement une CVE identifiée sur le composant : un risque de ReDoS (Regular Expression Denial of Service) lié au backtracking dans l'évaluation des fonctions match() et search(). Si un utilisateur contrôle les expressions régulières passées à ces fonctions, un pattern comme (A+)+ appliqué à une entrée du type aaaaaX peut provoquer une explosion exponentielle du nombre de tentatives (8 pour 4 caractères, 512 pour 10, 500 millions pour 30). La règle est simple : attention aux patterns dangereux dans votre regex.


En résumé

Le composant JsonPath de Symfony comble un manque réel dans l'écosystème PHP. Là où naviguer dans du JSON demandait jusqu'ici du code impératif verbeux et cassant, on dispose désormais d'un outil déclaratif, standardisé (RFC 9535), performant (décodage partiel), testable (assertions PHPUnit dédiées) et extensible (fonctions custom). C'est le pendant JSON du DomCrawler qui existait déjà pour HTML/XML et on se demande surtout pourquoi il a fallu attendre aussi longtemps.

Commentaires

Il n'y a actuellement aucun commentaire. Soyez le premier !