Skip to Content

Chapitre 8.2 — Turbo

Partie 8 : Symfony UX — Chapitre 2/4 Version de référence : Turbo 8 (Hotwire) / symfony/ux-turbo / Symfony 7.4.

⏱️ TL;DR

  • Turbo rend une app serveur fluide comme une SPA, sans écrire de JS, en trois briques.
  • Turbo Drive : intercepte liens et formulaires, charge la page en arrière-plan et remplace le <body> → navigation instantanée sans rechargement complet. Gratuit dès l’installation.
  • Turbo Frames : <turbo-frame id="x"> isole un fragment ; un lien/formulaire à l’intérieur ne met à jour que ce fragment (édition inline, chargement paresseux, pagination).
  • Turbo Streams : le serveur renvoie des fragments <turbo-stream action="append|replace|update|remove"> qui modifient des morceaux précis du DOM — en réponse HTTP ou poussés en temps réel via Mercure (Partie 12).
  • Avec l’attribut #[Broadcast] sur une entité, tout changement diffuse automatiquement un Turbo Stream → mises à jour live sans code.
  • Pour un dev Next.js : Turbo, c’est « la navigation et les mises à jour partielles d’une SPA », mais pilotées par du HTML serveur, pas par du state React.

🎯 Objectifs

  • Activer la navigation SPA-like avec Turbo Drive.
  • Isoler des fragments avec Turbo Frames.
  • Mettre à jour des morceaux de page avec Turbo Streams.
  • Entrevoir la diffusion temps réel via Mercure.

1. Installer

composer require symfony/ux-turbo

Ça branche Turbo dans l’importmap (AssetMapper, chapitre 7.3). Aussitôt, Turbo Drive est actif.

2. Turbo Drive : la navigation SPA-like, gratuite

Sans aucune ligne de code, Turbo intercepte les clics sur les liens et les soumissions de formulaires : au lieu d’un rechargement complet (flash blanc), il récupère la page en arrière-plan (fetch) et remplace le <body> en conservant le <head> (CSS/JS déjà chargés). Résultat : une navigation instantanée, comme une SPA, sur une app 100 % rendue serveur.

⚠️ Piège : comme le <head> et le JS persistent entre les pages, vos contrôleurs Stimulus se connectent/déconnectent au fil de la navigation (d’où l’importance du disconnect(), chapitre 8.1). Et un script inline « qui s’exécute au chargement » ne se rejoue pas comme avec un rechargement complet — placez la logique dans des contrôleurs Stimulus (avec connect()), pas dans des <script> de page.

3. Turbo Frames : mettre à jour un fragment

Un <turbo-frame> encapsule une portion de page. Toute navigation (lien, formulaire) à l’intérieur ne met à jour que ce cadre — le reste de la page ne bouge pas. Idéal pour l’édition inline, le chargement paresseux, la pagination d’une section.

{# la liste et le détail dans des frames distincts #} <turbo-frame id="booking_detail"> <a href="{{ path('admin_booking_edit', { id: booking.id }) }}">Modifier</a> <p>Statut : {{ booking.status.label }}</p> </turbo-frame>

Quand on clique « Modifier », Turbo charge la page d’édition mais n’en injecte que le <turbo-frame id="booking_detail"> correspondant : le formulaire d’édition remplace le contenu du cadre, sans toucher au reste. On peut aussi charger un frame paresseusement :

<turbo-frame id="stats" src="{{ path('admin_stats') }}" loading="lazy"> Chargement des statistiques… </turbo-frame>

Le contenu de /admin/stats est chargé quand le frame devient visible — parfait pour ne pas ralentir la page principale.

4. Turbo Streams : chirurgie du DOM

Là où un Frame remplace un cadre, un Stream applique des opérations ciblées sur plusieurs morceaux du DOM. Le serveur renvoie des balises <turbo-stream> :

{# réponse Turbo Stream après création d'une réservation #} <turbo-stream action="prepend" target="bookings_list"> <template> {{ include('booking/_row.html.twig', { booking: booking }) }} </template> </turbo-stream> <turbo-stream action="update" target="bookings_count"> <template>{{ count }}</template> </turbo-stream>

Actions disponibles : append, prepend, replace, update, remove, before, after. Ici, après avoir créé une réservation, on ajoute sa ligne en tête de la liste et on met à jour le compteur — le tout sans recharger la page. Côté contrôleur, on renvoie une réponse TurboStream :

use Symfony\UX\Turbo\TurboBundle; #[Route('/admin/bookings', name: 'admin_booking_create', methods: ['POST'])] public function create(Request $request, BookingCreator $creator): Response { $booking = $creator->create(/* ... */); if (TurboBundle::STREAM_FORMAT === $request->getPreferredFormat()) { $request->setRequestFormat(TurboBundle::STREAM_FORMAT); return $this->render('booking/create.stream.html.twig', [ 'booking' => $booking, 'count' => /* ... */, ]); // renvoie les <turbo-stream> } return $this->redirectToRoute('admin_booking_index'); // fallback sans Turbo }

Notez le fallback : si le client ne supporte pas Turbo, on redirige normalement. C’est le progressive enhancement — ça marche sans JS, et mieux avec.

5. Diffusion temps réel avec #[Broadcast]

Le plus spectaculaire : coupler Turbo Streams à Mercure (Partie 12) pour des mises à jour poussées en temps réel à tous les clients connectés. Il suffit d’un attribut sur l’entité :

use Symfony\UX\Turbo\Attribute\Broadcast; #[ORM\Entity] #[Broadcast] // ← diffuse un Turbo Stream à chaque création/màj/suppression class Booking { /* ... */ }

Désormais, quand un Booking change (n’importe où : back-office, API, worker), Symfony diffuse automatiquement un Turbo Stream via Mercure, et tous les navigateurs affichant la liste la voient se mettre à jour en direct — sans rechargement, sans polling, sans une ligne de JS. On met en place Mercure et ce mécanisme en Partie 12 ; retenez ici que Turbo + Mercure = temps réel déclaratif.

💡 Pour un dev Next.js : Turbo Streams + Mercure, c’est l’équivalent d’un WebSocket + mise à jour d’état + re-render — mais sans gérer de socket, de state, ni de réconciliation côté client. Le serveur envoie des morceaux de HTML avec une action (append/replace), et le navigateur les applique. Pour un back-office collaboratif, c’est bluffant de simplicité comparé à monter tout ça en React. (Pour le front public à état riche, vous garderez Next.js + Mercure côté client — Partie 12.)

✏️ Exercices

Exercice 1. Vous voulez qu’un clic sur « Modifier » dans une carte de réservation ouvre le formulaire d’édition dans la carte, sans recharger la page. Quel mécanisme Turbo utilisez-vous ?

✅ Solution

Un Turbo Frame : encapsuler la carte dans <turbo-frame id="booking_42">. Le lien « Modifier » (dont la cible d’édition est aussi rendue dans un frame de même id) fait que Turbo remplace uniquement le contenu de ce frame par le formulaire d’édition, sans toucher au reste de la page. C’est l’édition inline « gratuite ».

Exercice 2. Après création d’une réservation, vous voulez ajouter sa ligne en haut de la liste et mettre à jour un compteur, sans recharger. Quel mécanisme, et quelles actions ?

✅ Solution

Des Turbo Streams : le contrôleur renvoie deux balises — <turbo-stream action="prepend" target="bookings_list"> (ajoute la ligne en tête) et <turbo-stream action="update" target="bookings_count"> (met à jour le compteur). Prévoir un fallback (redirection) si le client ne supporte pas Turbo (progressive enhancement).

Exercice 3. Que fait #[Broadcast] sur une entité, et de quel composant a-t-il besoin ?

✅ Solution

#[Broadcast] diffuse automatiquement un Turbo Stream à chaque création/mise à jour/suppression de l’entité, de sorte que tous les clients connectés voient la mise à jour en temps réel (sans polling ni JS). Il s’appuie sur Mercure (Partie 12) pour pousser ces streams aux navigateurs.

🧠 Quiz de révision

1. Que fait Turbo Drive, et faut-il coder quelque chose ?

Il intercepte liens/formulaires, charge la page en arrière-plan et remplace le <body> (navigation SPA-like, sans flash). Actif dès l’installation, sans code.

2. À quoi sert un Turbo Frame ?

À isoler un fragment : une navigation à l’intérieur ne met à jour que ce cadre (édition inline, chargement paresseux, pagination de section).

3. Qu’est-ce qu’un Turbo Stream ?

Une réponse serveur composée de balises <turbo-stream action="append|replace|update|remove..."> qui modifient des morceaux précis du DOM, sans recharger la page.

4. Comment obtenir des mises à jour temps réel à tous les clients ?

Coupler Turbo Streams à Mercure — l’attribut #[Broadcast] sur une entité diffuse automatiquement les changements à tous les navigateurs connectés (Partie 12).

5. Qu’est-ce que le « fallback » d’un contrôleur qui renvoie du Turbo Stream ?

Le comportement sans Turbo (ex. une redirection) : l’app fonctionne même sans JS, et mieux avec — c’est le progressive enhancement.


Prochain chapitre : 8.3 — LiveComponents — des composants Twig réactifs qui se re-rendent au serveur.