---
title: "Créer un bundle Symfony UX : l'anatomie, et le cas concret de DataTables"
description: "La structure réelle d'un bundle Symfony UX — classe de bundle, configuration, contrôleurs Stimulus, assets — puis le cas concret d'UX DataTables, avec les pièges de recette Flex et d'access_control."
pubDate: 2024-09-21
updatedDate: 2026-09-27
family: 'php-symfony'
tags: []
heroImage: '/images/creer-un-bundle-symfony-ux-datatables.webp'
videoUrl: 'https://youtu.be/7ZyhQuNHflo'
faq:
  - question: "Qu'est-ce qui fait qu'un bundle Symfony est un bundle UX ?"
    answer: "Trois choses, documentées par Symfony : le composer.json doit porter le mot-clé symfony-ux ; les assets JavaScript doivent vivre dans un dossier assets avec un package.json contenant un bloc symfony ; et ce bloc décrit chaque contrôleur Stimulus (point d'entrée, activation, chargement eager ou lazy, autoimport) ainsi que les paquets de l'importmap. Sans le mot-clé, Flex ne traite pas le paquet comme un bundle UX et n'injecte pas les contrôleurs dans l'application."
  - question: 'Pourquoi mes routes de bundle renvoient-elles un 404 ?'
    answer: "Parce que le routage d'un bundle n'est jamais importé automatiquement par Symfony. L'application doit importer les routes explicitement, soit depuis un fichier de routes fourni par le bundle, soit depuis un service qui implémente RouteLoaderInterface. Un bundle qui oublie de le documenter laisse l'utilisateur devant des 404 difficiles à diagnostiquer."
  - question: 'Comment personnaliser un template de bundle sans le modifier ?'
    answer: "En le surchargeant dans le dossier templates/bundles/ de l'application, en conservant le chemin relatif au dossier templates du bundle. Symfony ajoute le préfixe ! pour étendre le template d'origine sans boucle infinie — @!AcmeUser/index.html.twig. C'est la raison pour laquelle un bundle expose ses templates dans un namespace Twig nommé plutôt que par des chemins absolus."
  - question: 'Où se trouve le fichier symfony.lock ?'
    answer: "À la racine du projet, à côté de composer.json — pas dans var/. Flex dérive son nom de composer.lock et seule la variable d'environnement SYMFONY_LOCKFILE change cet emplacement. Ce fichier trace les recettes installées et se commite avec le code."
  - question: "Les routes Ajax d'un bundle de tableau sont-elles protégées automatiquement ?"
    answer: "Non, et c'est un piège important. Le jeton envoyé par le tableau identifie la table, pas l'utilisateur : sans règle access_control dans l'application, n'importe qui peut interroger l'endpoint. La documentation d'UX DataTables insiste sur ce point — la sécurité des endpoints reste à la charge de l'application qui installe le bundle."
---

Un bundle Symfony UX, c'est un paquet PHP qui apporte du comportement front sans imposer de chaîne de
build : pas de webpack obligatoire, pas de Node exigé, les assets passant par AssetMapper et les
interactions par un contrôleur Stimulus. Voici son anatomie, puis ce que ça donne sur un vrai bundle —
celui qui intègre DataTables.net.

## L'anatomie d'un bundle, dans l'ordre

**Une classe, un namespace.** Un bundle est d'abord un namespace PHP : segment vendeur, segments
éventuels, puis un nom court terminé par `Bundle`, le tout en PSR-4. Depuis Symfony 6.1, la classe étend
`AbstractBundle` — étendre `Bundle` reste possible pour la compatibilité
([doc](https://symfony.com/doc/current/bundles.html)) :

```php
namespace Acme\BlogBundle;

use Symfony\Component\HttpKernel\Bundle\AbstractBundle;

class AcmeBlogBundle extends AbstractBundle
{
}
```

L'activation se fait par environnement dans `config/bundles.php`, et Flex s'en charge automatiquement à
l'installation ou à la désinstallation.

**La configuration, en deux endroits.** Dans `AbstractBundle`, l'arbre de configuration se déclare dans
`configure(DefinitionConfigurator $definition)` et les services dans
`loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder)` — deux
méthodes appelées à la compilation seulement. C'est aussi là que `prependExtension()` entre en jeu pour
préconcerner la configuration d'autres extensions, et que `build()` enregistre des
[compiler passes](https://symfony.com/doc/current/bundles/extension.html).

**Des routes qui ne s'importent jamais seules.** C'est écrit noir sur blanc dans la documentation :
« Routing is never automatically imported in Symfony »
([override](https://symfony.com/doc/current/bundles/override.html)). Un bundle expose donc soit un fichier
de routes, soit — plus élégant — un service implémentant `RouteLoaderInterface`, que l'application
référence comme `mon_bundle.route_loader::loadRoutes`. Un oubli à ce niveau produit des 404 que
l'utilisateur ne saura pas diagnostiquer.

**Des templates surchargeables.** Les fichiers d'un bundle sont accessibles par leur namespace Twig
(`@AcmeBlog/index.html.twig`) et se surchargent dans `templates/bundles/<NomDuBundle>/`, en conservant le
chemin relatif au dossier `templates/` du bundle. Le préfixe `!` permet d'étendre le template d'origine
sans boucle infinie.

**Un `composer.json` honnête.** Le type de paquet est `symfony-bundle`, l'autoload PSR-4 pointe vers
`src/`, et les dépendances optionnelles sont déclarées dans `suggest` plutôt qu'imposées dans `require`.

## Les trois règles qui font un « bundle UX »

Symfony publie une page dédiée
([Create a UX bundle](https://symfony.com/doc/current/frontend/create_ux_bundle.html)), et les règles
sont précises :

1. **Le mot-clé `symfony-ux`** dans le `composer.json`. Sans lui, Flex ne reconnaît pas le paquet comme
   bundle UX.
2. **Les assets dans un dossier `assets/`** accompagné d'un `package.json`, pour que Flex gère le paquet
   côté JavaScript à l'installation et à la mise à jour.
3. **Un bloc `symfony` dans ce `package.json`**, avec `controllers` — pour chaque contrôleur : `main`,
   `enabled`, `fetch` (`eager` ou `lazy`) et `autoimport` — et `importmap`, dont la liste doit correspondre
   aux `peerDependencies`.

Côté Stimulus, le nom d'un contrôleur assemble trois segments séparés par deux tirets — le vendeur, le
paquet, puis le contrôleur — et le filtre Twig stimulus_controller la résout à partir de cette notation.
[StimulusBundle](https://symfony.com/bundles/StimulusBundle/current/index.html)
fournit les fonctions et filtres Twig qui exposent les contrôleurs, charge ceux qui sont déclarés et gère
le chargement différé. Avec
AssetMapper, le bundle préconcène enfin la configuration pour que ses fichiers distribués soient servis
([AssetMapper](https://symfony.com/doc/current/frontend/asset_mapper.html)) : exactement le motif décrit
par la documentation.

## Cas concret : UX DataTables

[UX DataTables](https://github.com/pentiminax/ux-datatables) applique ce modèle pour intégrer
[DataTables.net](https://datatables.net/) dans Symfony. Ce que le dépôt contient réellement :

- `type: symfony-bundle`, mots-clés `datatables` et `symfony-ux`, licence MIT, PHP 8.3 minimum,
  `symfony/stimulus-bundle` en dépendance — la marque d'un bundle UX.
- Une classe `PentiminaxDataTablesBundle extends AbstractBundle` avec `$extensionAlias = 'data_tables'`,
  qui déclare l'arbre de configuration (`options`, `table_attributes`, `extensions`, `max_page_length`…)
  et enregistre trois compiler passes : registre des tables, validation des attributs, jeton CSRF.
- Des **routes chargées par un service** (`RouteLoaderInterface`), pas par un fichier statique : huit
  endpoints Ajax sous `/datatables/ajax` — données, édition, suppression, formulaire d'édition, détail,
  action groupée, export.
- Un contrôleur Stimulus (`@pentiminax/ux-datatables/datatable`) et une fonction Twig
  `render_datatable()` : la table se déclare en PHP, s'affiche en Twig, et le tri, la recherche et la
  pagination passent par Doctrine côté serveur.
- Des templates de modale (`modal/bs5/`, `modal/datatables/`), des traductions françaises et anglaises, et
  une documentation publiée sur [pentiminax.github.io/ux-datatables](https://pentiminax.github.io/ux-datatables/getting-started/installation/).

### Deux pièges qui valent le détour

**La sécurité des endpoints.** Les routes Ajax du bundle, sous /datatables/ajax, sont auto-enregistrées mais
**non protégées** : le jeton envoyé par le tableau identifie la table, pas l'utilisateur. Sans règle
`access_control` dans l'application, l'endpoint répond à n'importe qui. Un bundle d'administration qui
l'oublie dans sa documentation livre une faille par défaut.

**Les recettes Flex ne vieillissent pas toutes seules.** La recette du bundle publiée dans
`symfony/recipes-contrib` porte encore le manifeste de la version `0.43`, qui déclare l'ancien nom de
classe `DataTablesBundle` et copie un fichier de routes référençant `@DataTablesBundle/...` — alors que
le bundle a renommé sa classe en `PentiminaxDataTablesBundle`. Autrement dit : la recette s'installe, mais
elle ne peut pas activer correctement le bundle dans sa version actuelle. Le constat est vérifiable dans
le dépôt des recettes, et c'est une leçon générale : une recette Flex est du code, elle se met à jour
comme le reste, et une incompatibilité de manifeste ne se voit qu'à l'exécution.

Pour un bundle SaaS, cette mécanique est aussi ce qui rend l'adoption simple : `composer require`, la
recette active le bundle, l'application importe les routes et déclare ses tables. Le reste — colonnes
typées, filtres, traitement serveur — est du ressort du bundle, et c'est précisément le travail que
raconte la vidéo.

Le bundle vient d'un talk à l'API Platform Conference 2026 : les
[slides du talk](/conferences/ux-datatables-api-platform-conference-2026/) couvrent le chemin complet,
du premier tableau déclaré en PHP jusqu'au temps réel avec Mercure. Et si tu veux l'utiliser plutôt que le
construire, [Découvre UX DataTables](/projets/ux-datatables/) : la landing page résume l'installation, la
configuration et les colonnes disponibles.

Dans la vidéo, je construis le bundle pas à pas : structure, bundle class, assets Stimulus, colonnes
typées et traitement Doctrine côté serveur.

## Pour approfondir

**[Symfony et backend : du premier écran à la production](https://pentiminax.fr/blog/symfony-backend.md)**

Un parcours Symfony par objectif : Twig et Doctrine, projets concrets, déploiement continu, permissions et diagnostic à partir des articles du site.

[Parcourir le guide Symfony et backend](https://pentiminax.fr/blog/symfony-backend.md)

## Questions fréquentes

**Qu'est-ce qui fait qu'un bundle Symfony est un bundle UX ?**

Trois choses, documentées par Symfony : le composer.json doit porter le mot-clé symfony-ux ; les assets JavaScript doivent vivre dans un dossier assets avec un package.json contenant un bloc symfony ; et ce bloc décrit chaque contrôleur Stimulus (point d'entrée, activation, chargement eager ou lazy, autoimport) ainsi que les paquets de l'importmap. Sans le mot-clé, Flex ne traite pas le paquet comme un bundle UX et n'injecte pas les contrôleurs dans l'application.

**Pourquoi mes routes de bundle renvoient-elles un 404 ?**

Parce que le routage d'un bundle n'est jamais importé automatiquement par Symfony. L'application doit importer les routes explicitement, soit depuis un fichier de routes fourni par le bundle, soit depuis un service qui implémente RouteLoaderInterface. Un bundle qui oublie de le documenter laisse l'utilisateur devant des 404 difficiles à diagnostiquer.

**Comment personnaliser un template de bundle sans le modifier ?**

En le surchargeant dans le dossier templates/bundles/ de l'application, en conservant le chemin relatif au dossier templates du bundle. Symfony ajoute le préfixe ! pour étendre le template d'origine sans boucle infinie — @!AcmeUser/index.html.twig. C'est la raison pour laquelle un bundle expose ses templates dans un namespace Twig nommé plutôt que par des chemins absolus.

**Où se trouve le fichier symfony.lock ?**

À la racine du projet, à côté de composer.json — pas dans var/. Flex dérive son nom de composer.lock et seule la variable d'environnement SYMFONY_LOCKFILE change cet emplacement. Ce fichier trace les recettes installées et se commite avec le code.

**Les routes Ajax d'un bundle de tableau sont-elles protégées automatiquement ?**

Non, et c'est un piège important. Le jeton envoyé par le tableau identifie la table, pas l'utilisateur : sans règle access_control dans l'application, n'importe qui peut interroger l'endpoint. La documentation d'UX DataTables insiste sur ce point — la sécurité des endpoints reste à la charge de l'application qui installe le bundle.
