Symfony13 min

Symfony UX : Stimulus, Turbo, LiveComponent en pratique

Par Pierre-Arthur Demengel
Symfony UXStimulusTurboLiveComponent

Symfony UX represente la reponse de Symfony a la question "comment ajouter de l'interactivite sans construire une SPA React/Vue ?". En combinant Stimulus pour le JavaScript leger, Turbo pour la navigation sans rechargement et LiveComponent pour les composants reactifs cote serveur, UX offre une alternative pragmatique aux frameworks front-end complets. Voyons comment tout cela fonctionne en pratique avec Symfony 7.2.

L'ecosysteme Symfony UX

Symfony UX n'est pas un seul package mais un ecosysteme de composants :

  • Stimulus : framework JavaScript leger pour les interactions DOM
  • Turbo : navigation AJAX transparente et mises a jour partielles
  • LiveComponent : composants PHP reactifs sans ecrire de JavaScript
  • UX Chartjs : graphiques interactifs via Chart.js
  • UX Autocomplete : champs de recherche avec autocompletion
  • UX Typed : animation de frappe au clavier
  • UX Cropperjs : recadrage d'images
  • UX Dropzone : upload par drag-and-drop

L'installation se fait via Composer et Flex configure tout automatiquement :

# Installer Stimulus et Turbo
composer require symfony/stimulus-bundle
composer require symfony/ux-turbo

# Installer LiveComponent
composer require symfony/ux-live-component

# Installer des composants supplementaires
composer require symfony/ux-chartjs
composer require symfony/ux-autocomplete

Stimulus : le JavaScript juste necessaire

Stimulus est un framework JavaScript minimaliste cree par Basecamp (DHH). Au lieu de generer le HTML en JavaScript, Stimulus enrichit le HTML existant avec des comportements. La philosophie est simple : le HTML reste la source de verite, JavaScript ajoute l'interactivite.

Creer un controller Stimulus

// assets/controllers/counter_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['count'];
    static values = {
        initial: { type: Number, default: 0 }
    };

    connect() {
        this.count = this.initialValue;
        this.render();
    }

    increment() {
        this.count++;
        this.render();
    }

    decrement() {
        if (this.count > 0) {
            this.count--;
            this.render();
        }
    }

    render() {
        this.countTarget.textContent = this.count;
    }
}

Et le HTML correspondant dans votre template Twig :

{# templates/product/show.html.twig #}
<div data-controller="counter" data-counter-initial-value="1">
    <button data-action="click->counter#decrement">-</button>
    <span data-counter-target="count">1</span>
    <button data-action="click->counter#increment">+</button>
</div>

Les trois concepts cles de Stimulus :

  • Targets : references vers des elements DOM via data-{controller}-target
  • Values : parametres reactifs passes via data-{controller}-{name}-value
  • Actions : liaison evenements/methodes via data-action

Exemple avance : recherche en direct

// assets/controllers/search_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['input', 'results'];
    static values = {
        url: String,
        debounce: { type: Number, default: 300 }
    };

    #timeout = null;

    search() {
        clearTimeout(this.#timeout);
        this.#timeout = setTimeout(() => this.#fetchResults(), this.debounceValue);
    }

    async #fetchResults() {
        const query = this.inputTarget.value.trim();
        if (query.length < 2) {
            this.resultsTarget.innerHTML = '';
            return;
        }

        const response = await fetch(`${this.urlValue}?q=${encodeURIComponent(query)}`);
        this.resultsTarget.innerHTML = await response.text();
    }
}
<div data-controller="search" data-search-url-value="{{ path('app_search') }}">
    <input type="search"
           data-search-target="input"
           data-action="input->search#search"
           placeholder="Rechercher...">
    <div data-search-target="results"></div>
</div>

Turbo : navigation sans rechargement

Turbo se decompose en trois modules complementaires :

Turbo Drive

Turbo Drive intercepte automatiquement tous les clics sur les liens et les soumissions de formulaires. Au lieu de recharger la page, il effectue une requete AJAX et remplace le contenu de <body>. C'est transparent - aucune modification de code necessaire :

{# Activer Turbo Drive (actif par defaut) #}
{# Pour le desactiver sur un lien specifique : #}
<a href="/external-page" data-turbo="false">Lien classique</a>

{# Desactiver pour un formulaire #}
<form action="/legacy" data-turbo="false">...</form>

Turbo Frames

Turbo Frames permettent de mettre a jour une portion isolee de la page. C'est ideal pour les composants independants comme un panier, une liste paginee ou un formulaire inline :

{# templates/product/list.html.twig #}
<turbo-frame id="product-list">
    {% for product in products %}
        <div class="product-card">
            {{ product.name }} - {{ product.price }}€
            <a href="{{ path('app_product_show', {id: product.id}) }}">Voir</a>
        </div>
    {% endfor %}

    {# La pagination reste dans le frame #}
    <a href="{{ path('app_product_list', {page: next_page}) }}">Page suivante</a>
</turbo-frame>
{# templates/product/show.html.twig #}
{# Ce template est charge dans le frame product-list #}
<turbo-frame id="product-list">
    <h2>{{ product.name }}</h2>
    <p>{{ product.description }}</p>
    <a href="{{ path('app_product_list') }}">Retour</a>
</turbo-frame>

Seul le contenu a l'interieur du <turbo-frame> avec le meme id est remplace. Le reste de la page ne change pas.

Turbo Streams

Turbo Streams permet au serveur de pousser des modifications DOM precises. C'est le pont avec le temps reel via Mercure :

{# Reponse Turbo Stream (retournee par le controleur) #}
<turbo-stream action="append" target="notifications">
    <template>
        <div class="notification">
            Nouveau message de {{ user.name }}
        </div>
    </template>
</turbo-stream>

<turbo-stream action="replace" target="cart-count">
    <template>
        <span id="cart-count">{{ cart.count }}</span>
    </template>
</turbo-stream>

<turbo-stream action="remove" target="product-42"></turbo-stream>

Les actions disponibles : append, prepend, replace, update, remove, before, after.

Pour connecter Turbo Streams avec Mercure et obtenir des mises a jour en temps reel, consultez notre article sur Symfony Mercure.

LiveComponent : composants reactifs en PHP pur

LiveComponent est la fonctionnalite la plus impressionnante de Symfony UX. Elle permet de creer des composants interactifs entierement en PHP, sans ecrire de JavaScript :

<?php
// src/Twig/Components/ProductSearch.php
namespace App\Twig\Components;

use Symfony\UX\LiveComponent\Attribute\AsLiveComponent;
use Symfony\UX\LiveComponent\Attribute\LiveProp;
use Symfony\UX\LiveComponent\Attribute\LiveAction;
use Symfony\UX\LiveComponent\DefaultActionTrait;

#[AsLiveComponent]
class ProductSearch
{
    use DefaultActionTrait;

    #[LiveProp(writable: true)]
    public string $query = '';

    #[LiveProp(writable: true)]
    public string $category = '';

    #[LiveProp]
    public int $page = 1;

    public function __construct(
        private readonly ProductRepository $productRepository,
    ) {}

    public function getProducts(): array
    {
        return $this->productRepository->search(
            query: $this->query,
            category: $this->category,
            page: $this->page,
        );
    }

    #[LiveAction]
    public function nextPage(): void
    {
        $this->page++;
    }

    #[LiveAction]
    public function resetFilters(): void
    {
        $this->query = '';
        $this->category = '';
        $this->page = 1;
    }
}
{# templates/components/ProductSearch.html.twig #}
<div {{ attributes }}>
    <input type="text"
           data-model="query"
           placeholder="Rechercher un produit...">

    <select data-model="category">
        <option value="">Toutes les categories</option>
        <option value="electronics">Electronique</option>
        <option value="books">Livres</option>
    </select>

    <button data-action="live#action" data-live-action-param="resetFilters">
        Reinitialiser
    </button>

    <div class="results">
        {% for product in this.products %}
            <div class="product">{{ product.name }} - {{ product.price }}€</div>
        {% else %}
            <p>Aucun resultat.</p>
        {% endfor %}
    </div>

    <button data-action="live#action" data-live-action-param="nextPage">
        Charger plus
    </button>
</div>

Quand l'utilisateur tape dans le champ de recherche, le composant est automatiquement re-rendu cote serveur et le HTML mis a jour est envoye au navigateur. Tout cela sans ecrire une seule ligne de JavaScript.

LiveProp : les proprietes reactives

Les #[LiveProp] sont les donnees du composant. Le parametre writable: true indique que l'utilisateur peut modifier cette propriete via le front-end (liaison avec data-model). Les proprietes non-writable sont en lecture seule - toute tentative de modification cote client est rejetee par securite.

LiveAction : les methodes appelables

Les #[LiveAction] sont des methodes PHP appelables depuis le front-end via un clic ou un evenement. Elles modifient l'etat du composant, qui est ensuite re-rendu automatiquement.

Quand utiliser UX vs React/Vue ?

Voici mon avis apres avoir utilise les deux approches en production :

  • Symfony UX : formulaires dynamiques, filtres de recherche, tableaux de bord admin, e-commerce classique, applications CRUD avancees. Tout ce qui beneficie d'un rendu serveur avec des touches d'interactivite
  • React/Vue/SPA : editeurs visuels (type Figma), applications temps reel intensives (chat, jeux), configurateurs 3D, applications offline-first, interfaces avec des transitions complexes entre les ecrans

La bonne nouvelle : les deux approches sont compatibles. Vous pouvez utiliser Symfony UX pour 90% de votre application et integrer un composant React pour une fonctionnalite specifique qui le necessite. Pour cette approche hybride, consultez notre article sur l'architecture Symfony + React.

Configuration avec Webpack Encore

Symfony UX s'integre avec Webpack Encore via le bridge Stimulus :

// webpack.config.js
const Encore = require('@symfony/webpack-encore');

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')
    .addEntry('app', './assets/app.js')
    .enableStimulusBridge('./assets/controllers.json')
    .enableSingleRuntimeChunk()
;

module.exports = Encore.getWebpackConfig();
// assets/app.js
import './bootstrap.js';
import './styles/app.css';

// assets/bootstrap.js (genere automatiquement)
import { startStimulusApp } from '@symfony/stimulus-bridge';

export const app = startStimulusApp(require.context(
    '@symfony/stimulus-bridge/lazy-controller-loader!./controllers',
    true,
    /.[jt]sx?$/
));

Le stimulus-bridge detecte automatiquement les controllers dans assets/controllers/ et ceux exposes par les bundles UX installes. Pour approfondir la configuration Encore, lisez notre guide sur Webpack Encore et le front-end Symfony.

En resume

Symfony UX propose une alternative pragmatique aux SPA pour la majorite des applications web. Stimulus garde le JavaScript minimal et organise, Turbo elimine les rechargements de page, et LiveComponent apporte la reactivite sans quitter PHP. L'investissement en apprentissage est bien moindre qu'un framework front-end complet, et la productivite est excellente pour les projets classiques.

Vous souhaitez moderniser le front-end de votre application Symfony ? Consultez mes tarifs, decouvrez mes services, ou contactez-moi pour evaluer la meilleure approche pour votre projet.

Questions fréquentes

13 projets livrésGrand-Est & BelgiqueLighthouse >90Disponible immédiatement

Un projet en tête ?

Discutons de votre site web. Réponse garantie sous 24h.

Ou appelez directement :06 95 41 30 25

WhatsApp
Appeler