Aller au contenu principal
Retour au blog

Symfony 7.3 : ObjectMapper, JsonStreamer et SSE natifs

Flavien Métivier3 juin 20255 min

Symfony 7.3.0 est sorti le 29 mai 2025, et pour une fois le changelog mérite qu'on s'y arrête. Deux composants entièrement nouveaux — ObjectMapper et JsonStreamer —, un SSE natif dans HttpFoundation et une refonte de la console. Pas de revue exhaustive ici : voici les cinq features qui changent concrètement la façon d'architecturer une application Symfony en production.

ObjectMapper — du mapping DTO enfin intégré au framework

La couche de transformation objet-à-objet est l'un des patterns les plus répétitifs dans tout projet Symfony : hydratation de DTO depuis une entité Doctrine, mappage de payload API vers un objet métier, etc. Jusqu'ici, on choisissait entre une librairie externe (jane-php/automapper, cuyz/valinor) ou du code maison souvent difficile à maintenir. Symfony 7.3 embarque le composant symfony/object-mapper directement dans le framework, sans dépendance à déclarer.

Le principe repose sur l'attribut #[Map] posé sur les propriétés de la classe cible. Le service ObjectMapper est autowirable et prend en charge l'instanciation, le mapping de propriétés et les transformations imbriquées.

// src/DTO/UserDTO.php
namespace App\DTO;

use Symfony\Component\ObjectMapper\Attribute\Map;

final class UserDTO
{
    #[Map(source: 'firstName')]
    public string $first_name = '';

    #[Map(source: 'lastName')]
    public string $last_name = '';

    // même nom : pas d'attribut nécessaire
    public string $email = '';

    // transformation inline
    #[Map(source: 'createdAt', transform: [self::class, 'formatDate'])]
    public string $created_at = '';

    public static function formatDate(\DateTimeImmutable $d): string
    {
        return $d->format('Y-m-d');
    }
}
// src/Controller/UserController.php
use Symfony\Component\ObjectMapper\ObjectMapper;

final class UserController extends AbstractController
{
    public function __construct(private readonly ObjectMapper $mapper) {}

    #[Route('/api/users/{id}', methods: ['GET'])]
    public function show(User $user): JsonResponse
    {
        return $this->json($this->mapper->map($user, UserDTO::class));
    }
}

Ce que ça change architecturalement : les services UserTransformer / UserNormalizer écrits à la main dans des dizaines de projets peuvent être supprimés sans ajouter la moindre dépendance externe. Le composant supporte également le mapping de collections et les propriétés imbriquées via #[Map(target: AddressDTO::class)].

JsonStreamer et EventStreamResponse — performance et temps réel sans dépendance tierce

JsonStreamer (symfony/json-streamer) s'attaque à un goulot d'étranglement bien connu : le Serializer standard charge la collection entière en mémoire avant d'émettre le premier octet. JsonStreamer utilise des générateurs PHP pour émettre le JSON chunk par chunk — ce qui change tout dès qu'on sérialise plusieurs centaines d'entités ou qu'on expose un export volumineux en JSON.

// src/Controller/ReportController.php
use Symfony\Component\JsonStreamer\JsonStreamer;
use Symfony\Component\HttpFoundation\StreamedResponse;

final class ReportController extends AbstractController
{
    public function __construct(
        private readonly JsonStreamer $streamer,
        private readonly ReportRepository $repo,
    ) {}

    #[Route('/api/reports/export', methods: ['GET'])]
    public function export(): StreamedResponse
    {
        $rows = $this->repo->iterateAll(); // \Generator

        return new StreamedResponse(function () use ($rows): void {
            foreach ($this->streamer->encode($rows) as $chunk) {
                echo $chunk;
                flush();
            }
        }, 200, ['Content-Type' => 'application/json']);
    }
}

EventStreamResponse dans HttpFoundation règle un autre problème récurrent : le Server-Sent Events maison. Avant 7.3, on bricolait soit un bundle Mercure surdimensionné pour du SSE simple, soit une StreamedResponse avec les bons headers posés à la main. Désormais, c'est natif.

Besoin d'un expert Symfony ?

Réserver un appel
// src/Controller/JobController.php
use Symfony\Component\HttpFoundation\EventStreamResponse;
use Symfony\Component\HttpFoundation\ServerSentEvent;

#[Route('/api/jobs/{id}/progress', methods: ['GET'])]
public function progress(string $id): EventStreamResponse
{
    return new EventStreamResponse(function () use ($id): \Generator {
        do {
            $status = $this->queue->status($id);

            yield new ServerSentEvent(
                data: json_encode(['pct' => $status->percent], JSON_THROW_ON_ERROR),
                event: 'progress',
            );

            usleep(500_000);
        } while (!$status->done);

        yield new ServerSentEvent(data: '{"done":true}', event: 'done');
    });
}

La réponse positionne automatiquement Content-Type: text/event-stream, désactive le buffering et gère le keep-alive. Côté client, du JavaScript vanilla avec EventSource suffit — aucune librairie tierce nécessaire.

InvokableCommands et Messenger — la DX qui s'affine

La Console gagne les InvokableCommands : une commande devient une classe avec __invoke(), sans extends Command ni méthode configure(). Les arguments et options sont injectés directement en paramètres via #[Argument] et #[Option], ce qui rend les commandes testables unitairement comme n'importe quel service.

// src/Command/NotifyCommand.php
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Attribute\Argument;
use Symfony\Component\Console\Attribute\Option;
use Symfony\Component\Console\Style\SymfonyStyle;

#[AsCommand(name: 'app:notify', description: 'Envoie une notification ciblée')]
final class NotifyCommand
{
    public function __construct(
        private readonly NotificationService $notifier,
    ) {}

    public function __invoke(
        #[Argument] string $target,
        #[Option(shortcut: 'f')] bool $force = false,
        SymfonyStyle $io,
    ): int {
        $this->notifier->send($target, force: $force);
        $io->success("Notification envoyée à {$target}");

        return 0;
    }
}

Côté Messenger, le DeduplicateStamp empêche qu'un message identique soit dispatché plusieurs fois dans une fenêtre de temps donnée — indispensable pour les webhooks susceptibles de déclencher le même traitement en rafale.

use Symfony\Component\Messenger\Stamp\DeduplicateStamp;

// Le message ne sera traité qu'une seule fois par fenêtre de 60 s
$this->bus->dispatch(
    new GenerateReport($reportId),
    [new DeduplicateStamp(id: "report-{$reportId}", ttl: 60)],
);
  • ObjectMapper : remplace les hydrators maison et les librairies externes pour le mapping DTO
  • JsonStreamer : sérialisation haute performance en mémoire constante pour les grandes collections
  • EventStreamResponse : SSE natif dans HttpFoundation, zéro dépendance tierce
  • InvokableCommands : commandes Console sans boilerplate, avec injection par attributs PHP
  • DeduplicateStamp : idempotence native dans Messenger sans infrastructure Redis custom

Symfony 7.3 n'est pas une version de maintenance : ObjectMapper et JsonStreamer comblent deux lacunes architecturales réelles, et EventStreamResponse supprime une catégorie entière de code maison. Si vous maintenez une application Symfony 6.x ou 7.x et que la montée de version implique un audit de compatibilité, des dépréciations à corriger ou une revue des composants tiers remplaçables, c'est exactement ce que couvre le Bear Upgrade.

Cet article vous a plu ? Partagez-le !

Besoin d'un expert Symfony ?

20 ans d'expérience sur l'écosystème PHP/Symfony.