Création d'un composant
Créer un composant "Symfony UX" pour Quiz interactif

Notions théoriques
Qu'est-ce qu'un composant Twig dans Symfony UX ?
Symfony UX propose 2 types de composants réutilisables côté template : les Twig Components et les Live Components.
Dans ce cours, on travaille avec les Twig Components, qui constituent la base de tout composant Symfony UX personnalisé.
Un Twig Component est une classe PHP associée à un template Twig. Ensemble, ils forment une unité autonome et réutilisable que l'on peut appeler depuis n'importe quel template de l'application, comme une balise HTML personnalisée.
L'idée est simple : plutôt que de copier-coller du HTML et de la logique PHP dans plusieurs templates, on encapsule tout dans un composant.
On l'appelle ensuite avec une syntaxe concise :
<twig:Quiz :questions="questions" />
Cette ligne suffit à afficher un quiz complet, avec toute sa logique et son rendu visuel.
Les 2 packages nécessaires
Pour créer des composants Twig dans Symfony UX, il faut installer deux packages :
symfony/ux-twig-component: fournit la classe de baseAbstractComponentet le système de rendu des composants Twigsymfony/ux-stimulus-bundle: fournit Stimulus pour gérer les interactions JavaScript (déjà installé avec--webapp)
composer require symfony/ux-twig-component
Si le projet a été créé avec symfony new mon-projet --webapp, le bundle Stimulus est déjà installé. Seul ux-twig-component peut manquer.
La structure d'un Twig Component
Un composant Twig se compose de deux fichiers :
1. La classe PHP — placée dans src/Twig/Components/
// src/Twig/Components/Alert.php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Alert
{
public string $type = 'info';
public string $message = '';
}
2. Le template Twig — placé dans templates/components/
{# templates/components/Alert.html.twig #}
<div class="alert alert-{{ type }}">
{{ message }}
</div>
La convention de nommage est stricte : si la classe s'appelle Alert, le template doit s'appeler Alert.html.twig dans templates/components/. Symfony UX fait le lien automatiquement grâce à l'attribut #[AsTwigComponent].
L'attribut PHP #[AsTwigComponent]
L'attribut #[AsTwigComponent] (introduit en PHP 8.0) est ce qui transforme une classe PHP ordinaire en composant Twig. Il indique à Symfony UX que cette classe est un composant et lui permet de :
- Détecter automatiquement le template associé
- Exposer les propriétés publiques de la classe comme variables Twig
- Permettre l'appel du composant depuis un template avec
<twig:NomDuComposant />
On peut aussi personnaliser le nom du composant :
#[AsTwigComponent('mon-quiz')]
class Quiz { ... }
Ce qui permettrait de l'appeler avec <twig:mon-quiz />.
Les propriétés publiques : le pont PHP/Twig
Les propriétés publiques de la classe PHP sont automatiquement accessibles dans le template Twig du composant. C'est le mécanisme central de la communication entre la logique PHP et le rendu HTML.
#[AsTwigComponent]
class Quiz
{
public string $titre = 'Quiz sans titre';
public array $questions = [];
public bool $afficherScore = true;
}
Dans le template, on accède directement à titre, questions et afficherScore sans déclaration supplémentaire.
Seules les propriétés publiques sont exposées au template. Les propriétés protected et private restent inaccessibles depuis Twig — elles peuvent servir pour la logique interne de la classe.
Passer des données au composant
Lorsqu'on appelle un composant depuis un template parent, on lui passe des données via des attributs HTML-like :
{# Appel avec des valeurs statiques #}
<twig:Alert type="danger" message="Une erreur est survenue" />
{# Appel avec des variables Twig (préfixe : pour évaluer l'expression) #}
<twig:Alert :type="alertType" :message="alertMessage" />
Le préfixe : avant un attribut indique à Symfony UX d'évaluer l'expression Twig passée en valeur, plutôt que de la traiter comme une chaîne de caractères.
Sans le préfixe :, la valeur est toujours interprétée comme une chaîne de caractères. type="danger" passe la chaîne "danger", tandis que :type="monType" passe la valeur de la variable Twig monType.
Les méthodes dans un composant
Une classe de composant peut contenir des méthodes publiques, accessibles depuis le template :
#[AsTwigComponent]
class Quiz
{
public array $questions = [];
public function getNombreQuestions(): int
{
return count($this->questions);
}
}
Dans le template :
<p>Ce quiz contient {{ this.getNombreQuestions() }} question(s).</p>
Dans le template d'un composant, this désigne l'instance de la classe PHP du composant. On accède aux méthodes avec this.nomDeLaMethode() et aux propriétés avec nomDeLaPropriete.
Ajouter de l'interactivité avec Stimulus
Un Twig Component est statique : il génère du HTML côté serveur, sans interaction côté client. Pour ajouter de l'interactivité (comme révéler la bonne réponse au clic), on utilise Stimulus en ajoutant des attributs data-controller et data-action dans le template du composant.
<div data-controller="quiz">
<button data-action="click->quiz#verifier">Vérifier ma réponse</button>
<p data-quiz-target="resultat"></p>
</div>
Et dans un contrôleur Stimulus créé dans assets/controllers/quiz_controller.js :
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['resultat'];
verifier() {
this.resultatTarget.textContent = 'Bonne réponse !';
}
}
Cette combinaison Twig Component (logique PHP + rendu HTML) + Stimulus (interactivité JS) est le cœur de Symfony UX. Le PHP gère les données et la structure, JavaScript gère uniquement ce qui ne peut pas se faire côté serveur.
Exemple pratique
Création d'un composant Quiz complet
Cet exemple crée un composant Quiz réutilisable qui affiche une série de questions à choix multiple et révèle la bonne réponse au clic, grâce à Stimulus.
Étape 1 — Installer ux-twig-component
composer require symfony/ux-twig-component
Étape 2 — Créer la classe PHP du composant
Créer le fichier src/Twig/Components/Quiz.php :
<?php
namespace App\Twig\Components;
use Symfony\UX\TwigComponent\Attribute\AsTwigComponent;
#[AsTwigComponent]
class Quiz
{
public string $titre = 'Quiz';
/** @var array<array{question: string, options: string[], answer: string}> */
public array $questions = [];
public function getNombreQuestions(): int
{
return count($this->questions);
}
}
Étape 3 — Créer le template Twig du composant
Créer le fichier templates/components/Quiz.html.twig :
<div class="quiz-wrapper" data-controller="quiz">
<h2>{{ titre }}</h2>
<p><em>{{ this.getNombreQuestions() }} question(s)</em></p>
{% for index, q in questions %}
<div class="quiz-question" data-quiz-target="question" style="margin-bottom: 1.5rem; padding: 1rem; border: 1px solid #ddd; border-radius: 6px;">
<p><strong>{{ index + 1 }}. {{ q.question }}</strong></p>
{% for option in q.options %}
<label style="display: block; margin: 0.3rem 0; cursor: pointer;">
<input
type="radio"
name="question_{{ index }}"
value="{{ option }}"
data-answer="{{ q.answer }}"
data-action="change->quiz#selectionner"
data-quiz-target="option"
>
{{ option }}
</label>
{% endfor %}
<p data-quiz-target="feedback{{ index }}" style="margin-top: 0.5rem; font-weight: bold;"></p>
</div>
{% endfor %}
</div>
Étape 4 — Créer le contrôleur Stimulus
Créer le fichier assets/controllers/quiz_controller.js :
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
selectionner(event) {
const input = event.target;
const reponseChoisie = input.value;
const bonnereponse = input.dataset.answer;
// Trouver l'index de la question via le nom du champ radio
const nom = input.name; // ex: "question_0"
const index = nom.split('_')[1];
// Trouver le paragraphe feedback correspondant
const feedbackTarget = this.element.querySelector(
`[data-quiz-target="feedback${index}"]`
);
if (!feedbackTarget) return;
if (reponseChoisie === bonnereponse) {
feedbackTarget.textContent = '✓ Bonne réponse !';
feedbackTarget.style.color = 'green';
} else {
feedbackTarget.textContent = `✗ Mauvaise réponse. La bonne réponse était : ${bonnereponse}`;
feedbackTarget.style.color = 'red';
}
// Désactiver tous les boutons radio de cette question
const radios = this.element.querySelectorAll(`input[name="${nom}"]`);
radios.forEach(r => r.disabled = true);
}
}
Étape 5 — Utiliser le composant dans un contrôleur Symfony
Créer un contrôleur de test :
php bin/console make:controller QuizDemoController
Modifier src/Controller/QuizDemoController.php :
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class QuizDemoController extends AbstractController
{
#[Route('/quiz-demo', name: 'app_quiz_demo')]
public function index(): Response
{
$questions = [
[
'question' => 'Quel attribut PHP active un Twig Component ?',
'options' => ['#[Component]', '#[AsTwigComponent]', '#[TwigComponent]', '#[UxComponent]'],
'answer' => '#[AsTwigComponent]',
],
[
'question' => 'Où place-t-on les classes PHP des Twig Components ?',
'options' => ['src/Components/', 'src/Twig/Components/', 'src/UX/', 'templates/components/'],
'answer' => 'src/Twig/Components/',
],
[
'question' => 'Quel préfixe évalue une expression Twig dans un attribut de composant ?',
'options' => ['@', '$', '!', ':'],
'answer' => ':',
],
];
return $this->render('quiz_demo/index.html.twig', [
'questions' => $questions,
]);
}
}
Étape 6 — Appeler le composant dans le template
Modifier templates/quiz_demo/index.html.twig :
{% extends 'base.html.twig' %}
{% block title %}Démo Quiz{% endblock %}
{% block body %}
<div style="max-width: 700px; margin: 2rem auto; padding: 0 1rem;">
<h1>Démonstration du composant Quiz</h1>
<twig:Quiz
titre="Quiz Symfony UX"
:questions="questions"
/>
</div>
{% endblock %}
Étape 7 — Tester
symfony server:start
Ouvrir https://127.0.0.1:8000/quiz-demo. Chaque question affiche ses options sous forme de boutons radio. Au clic sur une option, le feedback s'affiche immédiatement en vert ou en rouge, et les options sont désactivées pour empêcher un second choix.
Le composant est entièrement réutilisable. Pour afficher un second quiz sur la même page, il suffit d'appeler <twig:Quiz titre="Quiz 2" :questions="autresQuestions" /> avec un autre tableau de questions.