guide/frontend-stack.md

Mettre en place une stack frontend Symfony sans build

Ce guide s'adresse Ă  qui connaĂźt dĂ©jĂ  Symfony et souhaite une couche frontend sans Ă©tape de build (pas de Webpack, Vite ou esbuild). L'idĂ©e : servir les assets directement, versionnĂ©s par hash, et n'ajouter du JavaScript que lĂ  oĂč c'est nĂ©cessaire.

Les combinaisons de versions sont laissées de cÎté volontairement : épinglez celles qui conviennent à votre projet et vérifiez leur compatibilité entre elles.


Ce que vous obtenez

Couche Outil
Gestion des assets Symfony AssetMapper
Modules JS Import Maps (natif navigateur)
Composants UI Bootstrap
IcĂŽnes Bootstrap Icons
Navigation SPA-like Turbo Drive + Turbo Frames
Comportements JS Stimulus
Templates Twig
CSS CSS natif (BEM partiel) + utilitaires Bootstrap

Aucun bundler : AssetMapper + importmap suffisent. Choix sobre et assumĂ© — la qualitĂ© d'implĂ©mentation prime sur l'outillage.


1. Poser AssetMapper + importmap

AssetMapper remplace Webpack Encore. Les dépendances sont déclarées dans importmap.php et résolues par le navigateur via <script type="importmap">.

Structure type de importmap.php :

// importmap.php (extrait)
return [
    'app' => ['path' => './assets/app.js', 'entrypoint' => true],
    '@hotwired/turbo'    => ['version' => '...'],
    '@hotwired/stimulus' => ['version' => '...'],
    'bootstrap'          => ['version' => '...'],
    'bootstrap/dist/css/bootstrap.min.css' => ['version' => '...', 'type' => 'css'],
    'bootstrap-icons/font/bootstrap-icons.min.css' => ['version' => '...', 'type' => 'css'],
];

Ajouter une dĂ©pendance : php bin/console importmap:require nom-du-package — met Ă  jour importmap.php automatiquement.

Un seul point d'entrée : assets/app.js. Tous les autres modules sont importés depuis ce fichier ou chargés via Stimulus. Gardez un entrypoint unique pour maßtriser ce qui part au navigateur.


2. Activer Turbo Drive puis Turbo Frames

Turbo Drive (global)

Navigation SPA-like sans code serveur : un clic charge la réponse en AJAX et remplace le <body>, sans rechargement complet. Il suffit de l'importer dans l'entrypoint :

import '@hotwired/turbo'; // démarre Turbo Drive globalement

Turbo Frames (navigation partielle)

Un frame charge et met à jour une portion de page indépendamment. Déclarez une zone à rafraßchir :

<turbo-frame id="ma-zone"> ... </turbo-frame>

Selon la version de Turbo, l'observateur de clic n'intercepte pas toujours les attributs data-turbo-frame. Vous pouvez piloter la navigation explicitement dans l'entrypoint :

const FRAME_NAV_PREFIXES = { 'ma-zone': '/prefixe-url' };

document.addEventListener('click', (event) => {
    const link = event.target.closest('a[href]');
    if (link?.dataset.turboFrame) {
        const frame = document.getElementById(link.dataset.turboFrame);
        if (frame && 'TURBO-FRAME' === frame.tagName) {
            event.preventDefault();
            frame.src = link.href;
            // Synchronise l'URL du navigateur (F5 / partage de lien)
            if (link.dataset.turboFrame in FRAME_NAV_PREFIXES) {
                window.history.pushState({}, '', link.href);
            }
        }
    }
});

Pensez à écouter popstate (window.history) pour resynchroniser le cadre avec le bouton Précédent.

RafraĂźchissement conditionnel d'un cadre

Pour recharger un cadre périodiquement tant qu'une opération de fond tourne, posez un marqueur dans le DOM et testez-le :

setInterval(() => {
    if (!document.querySelector('[data-operation-en-cours]')) return;
    document.getElementById('ma-zone')?.reload();
}, 10000);

Le marqueur est retirĂ© cĂŽtĂ© serveur quand l'opĂ©ration se termine : le polling s'arrĂȘte de lui-mĂȘme.


3. Ajouter des comportements avec Stimulus

Stimulus ajoute du JS ciblé sans framework global. Chaque controller est un module ES chargé à la demande.

Les controllers UX Symfony se déclarent dans assets/controllers.json :

{
    "controllers": {
        "@symfony/ux-...": { "nom": { "enabled": true, "fetch": "eager" } }
    }
}

Chargement paresseux : annotez un controller pour ne le charger que s'il est présent dans le DOM.

/* stimulusFetch: 'lazy' */

Pour des interactions simples et transverses (ex. sĂ©lection groupĂ©e de cases Ă  cocher), la dĂ©lĂ©gation d'Ă©vĂ©nements globale (click / change) suffit souvent, sans crĂ©er de controller dĂ©diĂ© — et fonctionne que la liste soit en pleine page ou dans un frame.

Protection CSRF compatible Turbo

Comme Turbo intercepte les soumissions, gérez le token CSRF en écoutant les événements Turbo :

// GénÚre et double-soumet un token CSRF (champ formulaire + cookie)
// Écoute turbo:submit-start / turbo:submit-end
export function generateCsrfToken(formElement) { /* ... */ }
export function generateCsrfHeaders(formElement) { /* ... */ }

4. Importer Bootstrap en module ES

Importez Bootstrap depuis l'importmap, pas depuis un CDN — vous gardez la main sur la version et le versioning par hash :

import 'bootstrap';                              // JS (modal, dropdown, collapse
)
import 'bootstrap/dist/css/bootstrap.min.css';  // CSS
import 'bootstrap-icons/font/bootstrap-icons.min.css'; // IcĂŽnes (font)

Les icĂŽnes s'utilisent directement dans les templates Twig :

<i class="bi bi-rss"></i>

Si vous exposez une icÎne par configuration cÎté PHP, renvoyez simplement sa classe :

public function icon(): string { return 'bi-rss'; }

5. Organiser le CSS et le charger conditionnellement

Séparez les feuilles par surface :

assets/styles/
├── app.css       ← rĂšgles globales (chrome Ă©ditable, skip link, utilitaires)
├── admin.css     ← styles back-office
└── front.css     ← styles surface publique

Ne servez au visiteur que ce dont il a besoin, via un dynamic import :

import 'bootstrap/dist/css/bootstrap.min.css';
import './styles/app.css';

if (isLoggedIn()) {
    import('./styles/admin.css'); // pas chargé pour les visiteurs anonymes
}

export function isLoggedIn() {
    return window.app?.user !== null;
}

Skip link accessible

Un lien d'Ă©vitement doit ĂȘtre hors Ă©cran par dĂ©faut et visible au focus clavier :

.skip-link {
    position: absolute;
    top: -1px;
    transform: translate(-50%, -200%); /* hors-écran par défaut */
    transition: all 0.1s;
    &:focus { transform: translate(-50%, 0%); } /* visible au focus clavier */
}

Neutraliser le style d'un contenu Ă©ditĂ© (Markdown → HTML)

Si des blocs (nav, footer) sont rédigés en Markdown puis rendus en HTML, neutralisez les marges pour qu'un lien Markdown ressemble au chrome natif :

.chrome-edite > :last-child { margin-bottom: 0; }
.chrome-edite p  { margin: 0; }
.chrome-edite ul { list-style: none; display: flex; flex-wrap: wrap; gap: .75rem; }
.chrome-edite-nav a { text-decoration: none; }

6. Structurer les templates Twig

Séparez par domaine et par surface, et préfixez les partials par _ :

templates/
├── admin/          ← back-office (nav, tableaux de bord, sections)
├── components/     ← partials rĂ©utilisables (_pagination.html.twig, 
)
├── <domaine>/      ← index.html.twig, show.html.twig
└── bundles/TwigBundle/Exception/
    ├── error.html.twig
    ├── error403.html.twig
    └── error404.html.twig

Convention : un fichier partial commence par _ (ex. _list.html.twig).

Héritage de layout (multi-surfaces)

Si plusieurs surfaces partagent un moteur mais un habillage diffĂ©rent, faites pointer chaque surface vers son layout_template et faites Ă©tendre ce layout par ses pages — chaque surface n'accĂšde qu'Ă  son propre chrome Ă©ditorial.


7. (Option) Web App Manifest

Pour proposer « Ajouter à l'écran d'accueil » sans passer PWA complÚte, un simple manifest suffit :

// site.webmanifest — mĂ©tadonnĂ©es + icĂŽnes (192×192, 512×512, apple-touch 180×180)

Sans Service Worker, pas de mode hors-ligne : c'est un choix assumé si vous ne visez pas l'usage offline.


Points de vigilance

  • Un seul entrypoint JS : tout ce qui n'y est pas rattachĂ© ne part pas au navigateur — c'est votre garde-fou de poids.
  • CompatibilitĂ© des versions : Turbo, Stimulus et Bootstrap Ă©voluent ; testez la navigation par frames et les soumissions CSRF aprĂšs chaque montĂ©e de version.
  • CSS conditionnel : vĂ©rifiez qu'aucun style back-office ne fuit vers les visiteurs anonymes.
  • AccessibilitĂ© : le skip link et l'ordre de focus se testent au clavier, pas seulement Ă  la souris.

Assistant documentaire

Posez une question sur la documentation. Les rĂ©ponses citent leurs sources — un clic ouvre le document Ă  gauche.