Storia Bundle : un environnement de composants Twig natif pour Symfony
Fractal et Storybook sont devenus des références pour travailler les composants UI en isolation. Mais dans un projet Symfony, ils introduisent une couche de friction qui finit par coûter cher, surtout quand on enchaîne les projets. Storia Bundle prend le problème à la racine : le rendu reste dans Twig, dans Symfony, sans émulation.
Le problème avec Fractal et Storybook sur des projets Symfony
Fractal est une bonne idée. On isole les composants, on les documente, on les teste visuellement. Le problème surgit quand il faut réconcilier ce qui a été développé dans Fractal avec ce qui tourne dans l'application Symfony. Les templates Twig de Fractal et ceux de l'appli ne partagent pas le même moteur de rendu. En pratique, ça veut dire copier-coller, adapter, re-tester. Sur un projet, c'est acceptable. Sur dix projets en parallèle, ça devient une source de bugs silencieux et de doubles maintenances.
Storybook pose un problème différent. C'est une machine bien huilée côté JavaScript, mais l'intégrer dans un projet Symfony implique d'embarquer un outillage JS conséquent, de former les développeurs PHP à son fonctionnement, et d'accepter que le rendu des composants Twig soit délégué à TwigJS. Or TwigJS n'est pas Twig. Les filtres customs, les extensions Symfony, les FormTypes : autant de fonctionnalités qui nécessitent des contournements ou des mocks. Et quand Twig sort une nouvelle version, TwigJS a souvent du retard.
Pour une agence qui gère plusieurs projets Symfony simultanément, ces frictions se multiplient : chaque projet a ses propres extensions Twig, ses FormTypes, ses conventions. Maintenir deux environnements de rendu en parallèle n'est pas tenable.
Storia Bundle : rester dans Symfony du début à la fin
Storia Bundle s'installe comme n'importe quel bundle Symfony. Pas de serveur Node à lancer en parallèle, pas de configuration webpack spécifique à l'outil.
composer require iq2i/storia-bundle
Une fois installé (Symfony Flex s'occupe de la configuration automatiquement), on crée un dossier storia/ à la racine du projet, avec deux sous-dossiers : components/ et pages/. Chaque composant est décrit dans un fichier YAML qui pointe vers le template Twig existant de l'application et définit ses variantes.
# storia/components/progress.yaml template: ui/progress.html.twig variants: small: args: height: 1.5 default: args: height: 2.5 large: args: height: 4
C'est le même template que celui utilisé en production. Pas de copie, pas d'adaptation. Si le template change, la preview change. Le rendu est géré par le moteur Twig de l'application, avec toutes ses extensions, ses filtres et ses helpers.
Twig Component et UX : aucune friction
Si le projet utilise les Twig Components de Symfony UX, Storia les supporte nativement. Il suffit de remplacer la clé template par component et de passer les arguments et les blocs HTML comme on le ferait dans un template.
# storia/components/button.yaml component: Button variants: plain: args: class: plain blocks: content: Plain button outline: args: class: outline blocks: content: Outline button
Les blocs Twig Component sont gérés proprement, y compris le bloc content par défaut. C'est un détail, mais ce genre de détail fait souvent la différence quand on travaille avec du code réel plutôt qu'avec des mocks.
Les FormTypes en isolation
C'est probablement la fonctionnalité la plus utile pour les agences qui font du form_theme. Storia permet de prévisualiser n'importe quel FormType Symfony directement, avec ses trois états générés automatiquement : état par défaut, champ désactivé, et champ en erreur.
# storia/components/form/choice.yaml form: Symfony\Component\Form\Extension\Core\Type\ChoiceType options: choices: 'In Stock': true 'Out of Stock': false
On peut également spécifier un form_theme directement dans le fichier YAML pour tester un thème de formulaire sur un champ précis, sans avoir à monter un formulaire complet dans un contrôleur. Pour qui a déjà perdu une heure à déboguer le rendu d'un ChoiceType dans un contexte global, c'est un vrai gain.
# storia/components/form/custom.yaml form: App\Form\CustomType form_theme: 'forms/custom_theme.html.twig' options: label: 'Custom Field'
Brancher les assets de l'application
Par défaut, Storia affiche les composants sans CSS ni JS. Pour lui faire utiliser les assets de l'application, il suffit d'override le template iframe.html.twig du bundle en suivant la convention Symfony standard.
{# templates/bundles/IQ2iStoriaBundle/iframe.html.twig #} {% extends '@!IQ2iStoria/iframe.html.twig' %} {# avec AssetMapper #} {% block javascripts %} {% block importmap %}{{ importmap('app') }}{% endblock %} {% endblock %} {# avec WebpackEncoreBundle #} {% block stylesheets %} {{ encore_entry_link_tags('app') }} {% endblock %} {% block javascripts %} {{ encore_entry_script_tags('app') }} {% endblock %}
Le
!dans@!IQ2iStoria/iframe.html.twigest la syntaxe Symfony pour n'override que les blocs souhaités sans remplacer le template entier. C'est une petite chose à ne pas oublier.
Données dynamiques avec l'Argument Resolver
Pour les composants qui ont besoin de données métier, passer des valeurs statiques dans les YAML peut vite devenir fastidieux ou peu représentatif. L'Argument Resolver de Storia permet d'appeler une méthode PHP directement depuis la configuration YAML. Si la classe est un service Symfony, le container est utilisé pour la résoudre.
# storia/components/product.yaml component: Product variants: default: args: product: App\Factory\ProductFactory::createDefault expensive: args: product: App\Factory\ProductFactory::createExpensive outOfStock: args: product: App\Factory\ProductFactory::createOutOfStock
Chaque méthode retourne un tableau ou un objet, exactement comme le ferait un contrôleur. La factory peut être statique ou enregistrée comme service. C'est une approche qui colle bien aux projets Symfony qui utilisent déjà des factories ou des data fixtures pour les tests.
Live reload : le navigateur suit sans qu'on lui demande
Depuis la dernière version, Storia intègre un watcher qui recharge automatiquement la preview quand un fichier change. Une commande suffit à le démarrer :
bin/console storia:watch
Ce qui est bien pensé ici, c'est que Storia distingue deux types de rechargement. Une modification dans le dossier storia/ (les fichiers YAML) déclenche un rechargement complet de la page, pour mettre à jour le menu et les métadonnées du composant. Une modification dans templates/ ou assets/ ne recharge que l'iframe, sans toucher au shell. Le rendu se met à jour, le reste reste en place.
Watching [page]: /path/to/project/storia Watching [iframe]: /path/to/project/templates Watching [iframe]: /path/to/project/assets Watching for changes… (Ctrl+C to stop) [14:23:07] Change detected: storia/components/badge.yaml (write) → page reload [14:23:41] Change detected: templates/ui/badge.html.twig (write) → iframe reload
Par défaut, les dossiers templates/ et assets/ sont surveillés pour les rechargements d'iframe. Si le projet a des extensions Twig dans src/Twig/, ou tout autre dossier pertinent, on peut étendre la liste dans la configuration :
# config/packages/iq2i_storia.yaml iq2i_storia: watch: - '%kernel.project_dir%/templates' - '%kernel.project_dir%/assets' - '%kernel.project_dir%/src/Twig'
Le dossier
storia/(défini pardefault_path) est toujours surveillé pour les rechargements de page. Il n'est pas nécessaire de l'ajouter à la listewatch.
Give it a try!
Storia Bundle est open source et disponible sur Packagist. La documentation couvre l'installation, la configuration et tous les cas d'usage décrits dans cet article. Si quelque chose manque ou ne fonctionne pas comme attendu, les issues GitHub sont ouvertes.
Documentation : https://ui-storia.com/
Dépôt GitHub : https://github.com/IQ2i/storia-bundle