Apprendre Symfony 6 : Gérer ses Assets avec AssetMapper
AssetMapper est le système recommandé par Symfony pour gérer vos assets. Il fonctionne entièrement en PHP, sans étape de build complexe ni dépendances.
Qu’est-ce que l’AssetMapper ?
L’AssetMapper est une fonctionnalité introduite avec Symfony 6.3
pour simplifier la gestion des assets frontend (CSS, JS, images, etc.).
L’AssetMapper permet d’écrire du JavaScript et du CSS moderne sans utiliser de bundler.
Avant AssetMapper : Webpack
Avant l’arrivée de l’AssetMapper, Symfony utilisait principalement des outils comme Webpack Encore, qui s’appuie sur Webpack, pour gérer efficacement les assets.
La tâche principale d’un bundler comme Webpack est de regrouper (ou « bundler ») les différents fichiers et ressources d’une application web (comme les fichiers JavaScript, CSS, images, etc.) en un ou plusieurs fichiers.
Outre cette agrégation de code, Webpack propose également de compiler et optimiser les fichiers frontend pour la production, avec des fonctionnalités avancées comme :
- Le tree shaking,
- La compilation / transpilation,
- La minification,
- La rétrocompatibilité
- Le formatage et le linting
- Etc.
En termes de performances, le bundling d’assets n’étant aujourd’hui plus une nécessité grâce au protocole HTTP/2
, un composant comme AssetMapper vient repenser à la simplification le processus de gestion d’assets traditionnel instauré par les bundlers.
Pourquoi et quand privilégier AssetMapper ?
Symfony a constaté que pour de nombreux projets, Webpack était trop complexe par rapport aux besoins réels.
L’AssetMapper répond aux besoins des développeurs qui veulent une gestion simple et rapide des assets.
- Simplicité : Contrairement à Webpack, l’AssetMapper n’a pas besoin de configuration complexe. Pas besoin de fichiers de configuration JavaScript volumineux et il s’occupe automatiquement du versioning des fichiers statiques (pour le « Cache Busting ») et de leur distribution dans le répertoire public.
- Performance : Pour des projets où l’on n’a pas besoin de fonctionnalités frontend avancées, l’AssetMapper est plus léger et rapide.
- Intégration : L’AssetMapper s’intègre parfaitement dans l’écosystème PHP et Symfony, ce qui le rend plus naturel pour les développeurs travaillant uniquement avec ce framework.
- Dépendances : Surcharger son projet Symfony avec Webpack s’avère plus lourd en termes de dépendances. Pour les devs « Pure-PHP » c’est un au revoir à l’écosystème Node.js qui fait du bien.
Installation
Pour installer AssetMapper, taper la ligne de commande suivante :
composer require symfony/asset-mapper symfony/asset symfony/twig-pack
Cette ligne de commande va créer les fichiers suivants :
📄 assets/app.js
: le fichier JavaScript principal📄 assets/styles/app.css
: le fichier CSS principal📄 config/packages/asset_mapper.yaml
: le fichier de configuration définissant le chemin vers les assets (par défaut le répertoire📂 assets
)📄 importmap.php
: le fichier contenant les mappings des modules JavaScript. Ce fichier définit où les fichiers sont situés (soit localement, soit via un CDN).
Cela met également à jour le layout 📄 templates/base.html.twig
:
{% block javascripts %}
{% block importmap %}{{ importmap('app') }}{% endblock %}
{% endblock %}
Fonctionnalités principales
Les 3 fonctionnalités principales de l’AssetMapper sont le mapping et référencement des assets, le versionning des assets et l’importmap.
Mapping et référencement
Le dossier 📂 assets
AssetMapper définit un ou plusieurs dossier(s) stockant les ressources (css, js, images…) destinées à être publiques.
Par défaut, le fichier de configuration 📄 config/packages/asset_mapper.yaml
définit ce répertoire comme étant 📂 assets
:
framework:
asset_mapper:
# The paths to make available to the asset mapper.
paths:
- assets/
Le dossier 📂 assets
mappé ci-dessus, va pouvoir être référencé dans les templates par la fonction Twig asset()
:
{{ asset('images/logo.svg') }}
Le chemin images/logo.svg
est relatif au dossier 📂 assets
.
Servir des assets en dev VS prod
Environnement de dev
En développement, AssetMapper, via le serveur de développement de Symfony, est capable de servir les assets automatiquement sans intervention manuelle.
Environnement de prod
En production, pour s’assurer que tous les assets sont correctement référencés et disponibles avant un déploiement, il est nécessaire d’exécuter la commande suivante :
symfony console asset-map:compile
AssetMapper va ainsi copier les fichiers des assets dans le répertoire 📂 public/assets
.
Versioning des assets
Les assets rendus publics par AssetMapper sont automatiquement versionnés, avec une URL incluant un hash pour le « Cache Busting ».
Par exemple le fichier 🖼️ assets/images/logo.svg
sera référencé avec la fonction Twig suivante :
{{ asset('images/logo.svg') }}
Et c’est en réalité l’URL 🖼️ images/logo-631a945f13718a4b3280af9c30ad9eaa.svg
qui sera inscrite dans le code HTML.
631a945f13718a4b3280af9c30ad9eaa
est un hash généré automatiquement par AssetMapper lors de la création/mise à jour d’un fichier. Cette technique permet d’invalider le cache existant (Cache Busting) car si le nom du fichier change, l’URL de la ressource change et la mise en cache n’est ainsi plus viable.
Importmap
Simplifier l’import de modules
L’AssetMapper facilite l’utilisation de la fonctionnalité native des navigateurs appelée « Importmap ». Cette fonctionnalité permet de simplifier l’usage de l’instruction JavaScript import
dans le navigateur en définissant des alias pour les chemins des modules.
En JavaScript natif, il faut spécifier un chemin relatif ou absolu pour importer un module :
import { multiplication, division } from './chemin/vers/calculate.js'
import confetti from "./vendor/js-confetti/js-confetti.js"
Cependant, pour les modules persos, et surtout tiers, écrire le chemin complet peut être fastidieux.
Traditionnellement, on utilise des bundlers comme Webpack pour définir ces alias lors de la phase de build sur un serveur Node.js. Cela permet d’écrire des imports plus simples :
import { multiplication, division } from 'calculate'
import confetti from 'js-confetti'
Aujourd’hui, ce n’est plus nécessaire, car les importmaps sont gérés directement par les navigateurs modernes, sans besoin de bundler.
Fonctionnement de l’importmap
1. Génération de l’importmap
Le processus de l’importmap débute avec la fonction Twig importmap()
, insérée dans le fichier 📄 base.html.twig
:
{% block importmap %}{{ importmap('app') }}{% endblock %}
La fonction Twig importmap()
va générer un script de type « importmap » mappant l’ensemble des dépendances contenues dans le fichier 📄 importmap.php
:
<?php
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
'@hotwired/stimulus' => [
'version' => '3.2.2',
],
'@symfony/stimulus-bundle' => [
'path' => './vendor/symfony/stimulus-bundle/assets/dist/loader.js',
],
'@hotwired/turbo' => [
'version' => '7.3.0',
],
];
Générera par exemple le code HTML suivant :
<script type="importmap" data-turbo-track="reload">
{
"imports": {
"app": "/assets/app-cacd8178e1119d803b07682fcaeee936.js",
"/assets/bootstrap.js": "/assets/bootstrap-c423b8bbc1f9cae218c105ca8ca9f767.js",
"/assets/styles/app.css": "data:application/javascript,",
"@symfony/stimulus-bundle": "/assets/@symfony/stimulus-bundle/loader-870999a02e9fc147c034d522826ea70d.js",
"@hotwired/stimulus": "/assets/vendor/@hotwired/stimulus/stimulus.index-b5b1d00e42695b8959b4a1e94e3bc92a.js",
"/assets/@symfony/stimulus-bundle/controllers.js": "/assets/@symfony/stimulus-bundle/controllers-9d42643c079ab11f27a3a9614f81cc2f.js",
"/assets/@symfony/ux-turbo/turbo_controller.js": "/assets/@symfony/ux-turbo/turbo_controller-ce5e32dafdec0b7752f02e3e2cb25751.js",
"/assets/controllers/hello_controller.js": "/assets/controllers/hello_controller-55882fcad241d2bea50276ea485583bc.js",
"@hotwired/turbo": "/assets/vendor/@hotwired/turbo/turbo.index-810f44ef1a202a441e4866b7a4c72d11.js"
}
}
</script>
On retrouve ici davantage de dépendances que celles listées dans 📄 importmap.php
car certaines sont référencées par des modules (comme 📄 bootstrap.js
et 📄 app.css
le sont par 📄 app.js
).
AssetMapper ajoute automatiquement un hash en suffixe des noms de fichiers pour optimiser le cache. C’est par exemple ici le cas de app-cacd8178e1119d803b07682fcaeee936
.js.
2. Génération de preloads
Les preloads permettent d’améliorer les performances en préchargeant les ressources (JavaScript, CSS, image, fonts…) essentielles avant qu’elles ne soient nécessaires.
Pour l’ensemble des fichiers (JavaScript uniquement) référencés par l’importmap, AssetMapper va générer des preloads.
Ces preloads sont définis au travers de la génération de balises <link rel="modulepreload" href="...">
.
<link rel="modulepreload" href="/assets/app-cacd8178e1119d803b07682fcaeee936.js">
<link rel="modulepreload" href="/assets/bootstrap-c423b8bbc1f9cae218c105ca8ca9f767.js">
<link rel="modulepreload" href="/assets/@symfony/stimulus-bundle/loader-870999a02e9fc147c034d522826ea70d.js">
...
3. Définition du point d’entrée
La fonction importmap('app')
va rechercher dans le fichier 📄 importmap.php
l’entrée associée au nom app
.
Cette entrée doit être définie en tant que point d’entrée ou « entrypoint » :
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
// ...
];
Le fichier 📄 assets/app.js
, associé au point d’entrée app
sera alors importé dans la page :
<script type="module">import 'app';</script>
4. Exécution du script initial
Cet import déclenche l’exécution du script, et, par extension, le chargement des assets indiqués dans 📄 app.js
. Par défaut, ce fichier charge les dépendances globales, le style et les scripts.
Chargement des dépendances globales
Le fichier 📄 bootstrap.js
contient les dépendances globales chargées par défaut par le framework Symfony.
import './bootstrap.js';
import "./styles/app.css";
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
Ces dépendances se résument aux technos :
- Framework JS Stimulus : Utilisé pour ajouter du comportement dynamique et réactif aux éléments de l’interface utilisateur en associant des contrôleurs JavaScript aux éléments HTML.
- Bibliothèque Turbo : Améliore les performances des applications web en minimisant les rechargements de page et en permettant des transitions fluides via le chargement partiel des pages.
Chargement du CSS
Place à l’importation du fichier 📄 app.css
contenant votre propre style CSS.
import './bootstrap.js';
import "./styles/app.css";
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
Chargement du JS
Place à l’importation de vos fichiers JS.
import './bootstrap.js';
import "./styles/app.css";
console.log('This log comes from assets/app.js - welcome to AssetMapper! 🎉');
Par défaut, un console.log()
est spécifié ici mais il est d’usage d’importer ses propres scripts organisés au sein de fichiers/modules distincts ou de charger des modules tiers.
Autres fonctionnalités
D’autres fonctionnalités majeures sont également disponibles en couplant le composant AssetMapper à des bundles Symfony. C’est notamment le cas pour l’utilisation de :
- TypeScript avec TypeScript Bundle
- Sass avec Sass Bundle
- Tailwind CSS avec Tailwind Bundle (Voir le tuto)
- Etc.
En revanche, pour des besoins frontend plus spécifiques comme la minification du code ou encore l’intégration de composants issus de frameworks JavaScript frontend tels que Vue, Svelte ou encore React (JSX), un bundler comme Wepback s’avèrera plus pertinent, voire essentiel.
Gérer un package tiers
Importer un package tiers
Les packages tiers peuvent proposer tout un tas de fonctionnalités frontend utiles pour vos projets Symfony.
Si vous souhaitez importer un package JavaScript, listé par exemple sur la plateforme npmjs.com, il faudra taper la commande :
symfony console importmap:require <nom-paquet>
Installons par exemple canvas-confetti
avec symfony console importmap:require canvas-confetti
. Cette commande va avoir pour effet de mettre à jour le fichier 📄 importmap.php
:
return [
'app' => [
'path' => './assets/app.js',
'entrypoint' => true,
],
// ...
'canvas-confetti' => [
'version' => '1.9.3',
],
];
Le paquet référencé dans ce fichier sera soit :
Téléchargé localement
Un paquet est téléchargé localement lorsque la clé path
indique un chemin (relatif au dossier 📂 assets
) vers lui ou lorsque seule une version
est spécifiée dans 📄 importmap.php
.
Les packages téléchargés localement sont situés dans le dossier 📂 assets/vendor
. Ce dossier est alors automatiquement exclu du versioning git grâce au fichier 📄 .gitignore
:
...
/assets/vendor/
Pour regénérer ces sources depuis un autre ordinateur (à la manière de la commande composer install
, permettant de regénérer les bundles listés dans le fichier 📄 composer.json
), il faudra taper la commande suivante :
symfony console importmap:install
Chargé via un lien CDN
Un paquet est importé via un lien CDN lorsqu’une clé url
suivie de l’URL vers la ressource distante est spécifiée dans 📄 importmap.php
Le package canvas-confetti
peut désormais être importé simplement depuis le fichier 📄 assets/app.js
:
import confetti from 'canvas-confetti';
confetti();
Si un package a plusieurs dépendances
Parfois, un package (comme Bootstrap) possède plusieurs dépendances, telle que @popperjs/core
.
La commande symfony console importmap:require <nom-paquet>
ajoutera à la fois le package principal et ses dépendances.
return [
// ...
'bootstrap' => [
'version' => '5.3.3',
],
'@popperjs/core' => [
'version' => '2.11.8',
],
'bootstrap/dist/css/bootstrap.min.css' => [
'version' => '5.3.3',
'type' => 'css',
],
]
Mapper du style CSS
Bien que l’importmap soit essentiellement utilisé pour des modules JavaScript, il peut également être utilisé pour mapper des feuilles de style CSS.
La clé type
devra alors être définie sur la valeur css
(js
étant sa valeur par défaut).
'bootstrap/dist/css/bootstrap.min.css' => [
'version' => '5.3.3',
'type' => 'css',
],
Mettre à jour un package tiers
Voir les mises à jour disponibles
Avant de mettre à jour un paquet, il est utile de taper la commande suivante :
symfony console importmap:outdated
Cette commande permet de lister les mises à jour disponibles pour certains paquets.
Mettre à jour l’ensemble des paquets
Pour mettre à jour l’ensemble des paquets, taper la commande :
symfony console importmap:update
Mettre à jour un paquet spécifique
Pour mettre à jour un paquet spécifique, préciser le/les nom(s) du/des paquet(s) en question avec la commande importmap:update
:
symfony console importmap:update <nom-paquet> ...
Supprimer un package tiers
Pour supprimer un paquet, taper la commande suivante :
symfony console importmap:remove <nom-paquet>