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.

Icône de calendrier
Intermédiaire
13 chapitres

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 :

copié !
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 :

base.html.twig
copié !
{% 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 :

asset_mapper.yaml
copié !
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() :

copié !
{{ 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 :

copié !
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 :

copié !
{{ 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 :

copié !
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 :

copié !
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 :

base.html.twig
copié !
{% 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 :

importmap.php
copié !
<?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 :

copié !
<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="...">.

copié !
<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 » :

importmap.php
copié !
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 :

copié !
<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.

assets/app.js
copié !
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.

assets/app.js
copié !
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.

assets/app.js
copié !
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 :

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 :

copié !
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 :

importmap.php
copié !
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 :

.gitignore
copié !
...
/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 :

copié !
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 :

app.js
copié !
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.

importmap.php
copié !
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).

importmap.php
copié !
'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 :

copié !
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 :

copié !
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 :

copié !
symfony console importmap:update <nom-paquet> ...

Supprimer un package tiers

Pour supprimer un paquet, taper la commande suivante :

copié !
symfony console importmap:remove <nom-paquet>