Live Components : JavaScript, moi non plus
Créez des interfaces réactives en PHP avec les Live Components de Symfony : pas de JavaScript, juste du Twig et des composants dynamiques.
Publié le
Temps de lecture 3 min
Les Live Components sont la digne évolution des Twig Components. Ils permettent de franchir la dernière étape pour vous prendre pour un développeur React en créant des interfaces réactives, le tout sans écrire une ligne de JavaScript (un vrai confort) et en restant dans votre univers PHP habituel.
Fini le temps où il fallait choisir entre une page statique ou l'artillerie lourde d'un framework JS complet pour un simple champ de recherche dynamique. Voyons comment Symfony vous permet de rester maître à bord, de la classe PHP jusqu'au rendu final.
Qu'est-ce qu'un Live Component ?
Pour faire simple : c'est un Twig Component qui a le super-pouvoir de se re-rendre lui-même qui a le super-pouvoir de se re-rendre lui-même.
Lorsqu'un utilisateur interagit avec lui (en tapant dans un champ, en cliquant sur un bouton), le composant n'envoie pas de JSON à un obscur script JS. À la place, il envoie un appel AJAX (fête des fleurs) à votre serveur Symfony. Le serveur exécute la logique PHP, re-calcule le template Twig du composant, et renvoie le nouvel HTML. Côté client, un micro-script (Stimulus, sous le capot) intercepte ce HTML et "morphe" le DOM existant pour n'appliquer que les changements.
Le résultat ? L'interface se met à jour en temps réel, et vous n'avez écrit que du PHP et du Twig.
Installation et Configuration Complète
C'est sans doute le plus simple. Pour commencer, vous n'avez besoin que d'une seule commande Composer :
composer require symfony/ux-live-component
Ensuite, la configuration dépend de votre installation JavaScript :
- Si vous utilisez AssetMapper (la configuration par défaut sur Symfony 6.3+), vous n'avez rien à faire. Le bundle est automatiquement configuré et prêt à l'emploi : l'élégance de Symfony flex.
- Si vous utilisez WebpackEncore, vous devez installer les dépendances JS et relancer Encore :
npm install --force
npm run watch
Cas particulier : Les sites internationalisés
Si votre site gère les langues via un paramètre de route (par exemple, /fr/... ou /en/...), vous devez l'indiquer au routeur des Live Components.
Modifiez le fichier config/routes/ux_live_component.yaml pour y ajouter le préfixe /{_locale} :
# config/routes/ux_live_component.yaml
live_component:
resource: '@LiveComponentBundle/config/routes.php'
prefix: /{_locale}/_components # Ajoutez cette ligne
requirements:
_locale: '%app.supported_locales%' # Assurez-vous que ce paramètre existe
Créer votre premier Live Component
La façon la plus rapide de démarrer est d'utiliser le MakerBundle :
bin/console make:twig-component --live Header
Cette commande va créer pour vous :
- La classe du composant :
src/Twig/Components/Header.php - Le template du composant :
templates/components/Header.html.twig
La classe générée contiendra déjà tout le nécessaire pour "devenir vivant" :
<?php
namespace App\Twig\Components;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent]
final class Header
{
use DefaultActionTrait;
}
Si vous préférez le faire manuellement, assurez-vous de plusieurs choses :
- Remplacez
#[AsTwigComponent]par#[AsLiveComponent]. - Ajoutez use DefaultActionTrait;. Ce trait fournit l'action par défaut
(__invoke)qui permet au composant de se re-rendre. - Dans votre template Twig, ajoutez
{{ attributes }}au seul et unique élément racine. C'est cxrucial : c'est ce qui permet au contrôleur Stimulus de s'accrocher à votre composant.
{# templates/components/Header.html.twig #}
<div {{ attributes }}>
Mon composant est vivant !
</div>
Les Concepts Clés pour Bâtir la Réactivité
Pour rendre votre composant interactif, vous devez jongler avec trois concepts principaux.
#[AsLiveComponent] : le laissez-passer
Comme vous l'avez vu, c'est l'attribut qui déclare votre classe comme un composant.
#[AsLiveComponent(
name: 'Layout:Header', // Le nom pour l'appeler en Twig <twig:Layout:Header />
template: 'components/Layout/Header.html.twig', // Le chemin du template
)]
#[LiveProp] : La gestion de l'état (state)
C'est le cœur de votre composant. Une #[LiveProp] est une propriété "vivante" : sa valeur est conservée entre chaque re-rendu. C'est l'état de votre composant.
use Symfony\UX\LiveComponent\Attribute\LiveProp;
#[LiveProp]
public bool $menuOpen = false;
#[LiveProp]
public string $query = '';
Ces propriétés doivent être "sérialisables" : des types simples (string, int, bool, array) ou des objets (DTO, Entités).
Data-binding avec data-model
C'est ici que la magie opère. Vous pouvez lier directement un champ de formulaire à une #[LiveProp].
<input
type="text"
data-model="query"
placeholder="Rechercher des billets..."
/>
Grâce à data-model="query", à chaque fois que l'utilisateur tape dans ce champ, la propriété $query de votre composant PHP est mise à jour, et le composant se re-rend automatiquement.
Important : Pour des raisons de sécurité, ce lien ne fonctionne que si vous l'autorisez explicitement en PHP avec writable: true.
#[LiveProp(writable: true)]
public string $query = '';
Debouncing automatique : Par défaut, Symfony est malin. Il ne lance pas une requête à chaque touche. Il attend une pause de 150ms (debouncing) avant de re-rendre le composant. Vous pouvez modifier ce comportement :
data-model="debounce(300)|query": attend 300ms.data-model="on(change)|query": attend que l'utilisateur quitte le champ (événement change).
#[LiveAction] : Les Actions Utilisateur
Une #[LiveAction] est une méthode publique que vous pouvez déclencher depuis votre template Twig, généralement au clic d'un bouton.
<?php
// Dans votre classe Component
use Symfony\UX\LiveComponent\Attribute\LiveAction;
#[LiveAction]
public function toggleMenu(): void
{
$this->menuOpen = !$this->menuOpen;
}
Côté Twig, vous l'appelez ainsi :
<button
type="button"
data-action="live#action"
data-live-action-param="toggleMenu"
>
Ouvrir / Fermer
</button>
Au clic, le composant exécute la méthode toggleMenu() en PHP, puis se re-rend avec la nouvelle valeur de $menuOpen.
Propriétés Calculées avec #[ExposeInTemplate]
Par défaut, les méthodes publiques ne sont pas accessibles dans Twig. L'attribut #[ExposeInTemplate] vous permet d'exposer des "propriétés calculées" tel que getSearchResults() à votre template.
Ces méthodes sont mises en cache durant le cycle de vie du composant : même si vous l'appelez 10 fois dans votre Twig, la méthode ne sera exécutée qu'une seule fois.
Cas Pratique : un header de navigation vivant
La Classe PHP
Toute la magie et la logique métier se passe dans la classe PHP du composant.
<?php
namespace App\Twig\Components;
use App\Repository\CategoryRepository;
use App\Repository\PageRepository;
use App\Repository\PostSearchRepository;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\ExposeInTemplate;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\DefaultActionTrait;
#[AsLiveComponent(
name: 'Layout:Header',
template: 'components/Layout/Header.html.twig',
)]
final class Header
{
use DefaultActionTrait; // Le point d'entrée par défaut pour le re-rendu
public function __construct(
private readonly PostSearchRepository $postSearchRepository, // Pour chercher efficacement
private readonly PageRepository $pageRepository, // Pour les pages statiques
private readonly CategoryRepository $categoryRepository, // Pour le tri des catégories
private readonly UrlGeneratorInterface $urlGenerator, // Pour générer les URLs
) {}
// --- PROPRIÉTÉS D'ÉTAT (STATE) ---
#[LiveProp(writable: true)]
#[ExposeInTemplate]
public bool $menuOpen = false; // Le menu est-il ouvert ?
#[LiveProp(writable: true)]
#[ExposeInTemplate]
public bool $searchMode = false; // Mode recherche activé ?
#[LiveProp(writable: true)]
#[ExposeInTemplate]
#[Assert\Length(max: 100)]
public string $query = ''; // La requête de recherche
// --- ACTIONS UTILISATEUR ---
#[LiveAction]
public function toggleMenu(): void
{
$this->menuOpen = !$this->menuOpen;
// Logique métier : fermer le menu = sortir du mode recherche
if (!$this->menuOpen) {
$this->searchMode = false;
$this->query = '';
}
}
#[LiveAction]
public function activateSearch(): void
{
$this->menuOpen = true; // Ouvre le menu pour afficher la recherche
$this->searchMode = true; // Active le mode recherche
}
#[LiveAction]
public function closeSearch(): void
{
$this->searchMode = false;
$this->query = '';
$this->menuOpen = false;
}
// --- PROPRIÉTÉS CALCULÉES ---
#[ExposeInTemplate('searchResults')]
public function getSearchResults(): array
{
// On évite les requêtes inutiles si la recherche est trop courte
if (\strlen($this->query) < 3) {
return [];
}
// On limite le nombre de résultats
return $this->postSearchRepository->search($this->query, 8);
}
#[ExposeInTemplate]
public function hasMinimumChars(): bool
{
return \strlen($this->query) >= 3;
}
#[ExposeInTemplate]
public function hasResults(): bool
{
// Notez l'appel à $this->getSearchResults()
// Grâce au cache, la recherche n'est pas exécutée deux fois.
return $this->hasMinimumChars() && $this->getSearchResults() !== [];
}
#[ExposeInTemplate]
public function hasNoResults(): bool
{
return $this->hasMinimumChars() && $this->getSearchResults() === [];
}
}
Le template Twig
Côté template, vous liez tout cela avec data-action et data-model.
{# La racine du composant avec {{ attributes }} #}
<header {{ attributes }} class="sticky top-0 z-40 w-full backdrop-blur">
{# ... code de la nav ... #}
{# Bouton hamburger avec une action #}
<button
type="button"
class="inline-flex items-center justify-center p-2 rounded-md text-gray-900"
data-action="live#action"
data-live-action-param="toggleMenu"
aria-expanded="{{ menuOpen ? 'true' : 'false' }}"
>
{# Icône qui change selon l'état #}
{% if menuOpen %}
<svg class="h-6 w-6"></svg>
{% else %}
<svg class="h-6 w-6"></svg>
{% endif %}
</button>
{# ... #}
{# La recherche réactive #}
{% if searchMode %}
<input
type="text"
data-model="query" {# Liaison bidirectionnelle avec $query #}
placeholder="Rechercher des billets..."
class="search-input-gradient block w-full pl-10 pr-3 py-3 text-lg"
autocomplete="off"
spellcheck="false"
autofocus
/>
{# Affichage conditionnel basé sur les propriétés calculées #}
{% if not hasMinimumChars %}
<div class="text-center py-6">
<p class="text-sm text-gray-600">
Tapez au moins 3 caractères pour commencer la recherche...
</p>
</div>
{% elseif hasResults %}
<div class="space-y-6 pb-4">
{% for post in searchResults %}
<article class="group relative focus:outline-none py-4 lg:py-5">
</article>
{% endfor %}
</div>
{% elseif hasNoResults %}
<div class="text-center py-12">
<h3 class="text-base font-medium text-gray-900 mb-2">Aucun résultat</h3>
<p class="text-sm text-gray-500">
Aucun billet ne correspond à votre recherche.
</p>
</div>
{% endif %}
{% endif %}
</header>
Optimisations possibles
Il y a beaucoup de choses perfectibles, voici pêle-mêle quelques idées.
Validation des Données
On peut utiliser Symfony Validator directement sur les #[LiveProp].
#[LiveProp(writable: true)]
#[Assert\Length(max: 100)]
#[Assert\NotBlank(allowNull: true)]
public string $query = '';
Pour une validation plus complexe, on peut utiliser le ValidatableComponentTrait et appeler $this->validateField('query') dans vos actions.
Gestion des états Complexes
La logique dans toggleMenu() est un parfait exemple : une action doit toujours laisser le composant dans un état cohérent. Si vous fermez le menu, vous devez aussi réinitialiser l'état de la recherche.
Optimisation des requêtes
Ne lancez jamais de requêtes inutiles : \strlen($this->query) < 3 est la meilleure pratique qui soit. Elle évite de surcharger la base de données.
États de chargement (loading states)
Que se passe-t-il pendant les 150ms de l'appel AJAX ? Vous pouvez donner un feedback visuel à l'utilisateur très simplement.
<div {{ attributes }}>
<input data-model="query" type="text" />
{# Ce span ne sera visible que pendant le re-rendu #}
<span data-loading="show">
Chargement...
</span>
{# Vous pouvez aussi ajouter/retirer des classes #}
<div data-loading="addClass(opacity-50)">
{# Les résultats de la recherche #}
</div>
</div>
Synchronisation avec l'URL
Pour une page de recherche, vous voulez souvent que la requête soit dans l'URL pour être partageable. C'est trivial :
#[LiveProp(writable: true, url: true)]
#[ExposeInTemplate]
#[Assert\Length(max: 100)]
public string $query = '';
En ajoutant url: true, votre propriété $query sera automatiquement synchronisée avec les paramètres de l'URL (?query=ma-recherche).
Le mot de la fin
Les Live Components, c'est la preuve que Symfony peut faire du front-end moderne sans sacrifier la simplicité : des interfaces réactives, de la logique métier propre, et surtout, plus d'excuses pour mélanger du code obscur dans vos templates.
C'est un peu comme React, mais en mieux organisé. C'est un peu comme Vue, mais avec du typage strict. Bref, c'est du pur Symfony : élégant, puissant, et ça marche du premier coup (ou presque).