LOÏC.SAPONE

Symfony Developer

Retour

Storia Bundle : un environnement de composants Twig natif pour Symfony

10 min de lecture

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.

Screenshot d'illustration de l'interface de Storia

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.twig est 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 par default_path) est toujours surveillé pour les rechargements de page. Il n'est pas nécessaire de l'ajouter à la liste watch.

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.

Vous avez aimé cet article ?

Je publie régulièrement des articles sur Symfony, PHP et l'architecture logicielle. N'hésitez pas à me suivre sur Bluesky ou GitHub pour ne rien manquer.