04.1 - Symfony : gestion des assets avec AssetMapper
Objectifs
- ✅ Découvrir le nouveau gestionnaire d'assets de Symfony
- ✅ Installer des dépendances
- ✅ Organiser tes fichiers CSS ou SCSS
- ✅ Gérer les images de ton webdesign
Introduction
Intégré depuis la version 6.3, AssetMapper est le bundle officiel pour gérer les assets dans Symfony. Le concept est né du constat que dorénavant tous les navigateurs Internet acceptaient l'exécution de code JavaScript moderne. Cette quête revient sur ce « JavaScript moderne » et présente en préambule ce que sont les modules en JavaScript pour pouvoir mieux comprendre comment fonctionne AssetMapper. Tu verras ensuite comment gérer les fichiers CSS, JS, et tout type d'asset, mais également les dépendances frontend et comment travailler avec des fichiers SCSS dans une application Symfony.
Cette quête est assez longue en terme de contenus car nous avons souhaité aborder un maximum d'explications. Mais tu vas voir qu'au final il n'y a pas grand chose à faire 😉.
Sommaire
Un peu d'histoire : ECMAScript 6
Lorsque l'on parle de JavaScript moderne en introduction de cette quête, on entend les fonctionnalités disponibles depuis la deuxième mise à jour majeure du langage, sous le nom ECMAScript 6 (ES6) publiée en 2015. La version ES6 de JavaScript ne date donc pas d'hier, mais son implémentation native dans les principaux navigateurs Internet a mis un peu de temps. Il était recommandé aux développeurs web il n'y a encore pas si longtemps, d'avoir recours à un bundler tel que Webpack, le plus connu, pour être certain de produire un code compatible avec tous les navigateurs.
La liste des fonctionnalités qui avait été publiée lors de cette mise à jour est assez longue. N'hésite pas à consulter la ressource ci-dessous pour en connaître le contenu.
Également connue sous le nom ECMAScript 2015
Voici quels en étaient les points forts dont tu as sûrement déjà entendu parlé :
letouconstremplacent le motvarpour déclarer une variable.- les fonctions fléchées
- la boucle for…of (eh oui elle n'existait pas avant en JavaScript 🤓)
- le Spread (...) Operator
- sans oublier les Classes ☝️ , très très proches de ce que tu connais avec la POO en PHP
- et bien plus encore…
Mais surtout une fonctionnalité très importante sur laquelle repose AssetMapper : les Modules
Les modules ES6
Les modules JavaScript nous permettent d'organiser notre code JavaScript en fichiers séparés grâce à un système d'import et d'export.
- 1
Version classique
Prenons l'exemple d'un fichier
greet.jsdans lequel serait déclarée une fonctionsayHello().js//greet.js const sayHello = (name) => { return 'Hello ' + name; };Jusqu'ici tu as appris à intégrer du code JavaScript grâce à la balise
<script>et son attributsrc. Pour appeler cette fonction dans une page HTML, tu aurais naturellement écrit ce qui suit pour disposer immédiatement de la fonctionsayHello():html<!--index.html--> <script src="./greet.js"></script> <script> console.log(sayHello("John")); </script> - 2
Import/export d'un module
Grâce au système d'import et d'export des modules JavaScript, on obtient exactement le même résultat. Voici ce que cela donne.
html<script type="module"> import sayHello from "./greet.js"; console.log(sayHello("John")); </script>jsconst sayHello = (name) => { return 'Hello ' + name; }; export default sayHello;Si on pousse un peu plus loin la réflexion, on peut tout à fait imaginer la structure suivante où l'on importe un module qui importe un autre module :
html<script type="module"> import "./app.js"; </script>jsimport sayHello from "./greet.js"; console.log(sayHello("John"));jsconst sayHello = (name) => { return 'Hello ' + name; }; export default sayHello;Dans cet exemple, on dit que le fichier
app.jsjoue le rôle de point d'entrée. Il ne contient qu'un minimum de code et principalement des imports d'autres fichiers qui, eux, contiennent la logique métier. (💡 Retiens bien cette notion de point d'entrée, nous en reparlerons un peu plus tard). Et il n'y aurait pas de limites à ces imports en cascade. Un peu comme ce que tu as appris à faire en PHP grâce aux namespace et aux use.Et de la même manière qu'en PHP, la caractéristique de ce système d'import/export des modules est que les variables (et donc les fonctions ou les classes) restent isolées dans le scope des fichiers. Ils doivent absolument être exportées et importées pour être utilisables ailleurs.
Reprenons notre exemple et ajoutons une fonction
sayGoodbye()au fichiergreet.jscomme ceci :html<script type="module"> import "./app.js"; </script>jsimport sayHello from "./greet.js"; console.log(sayHello("John")); console.log(sayGoodbye("John"));jsconst sayHello = (name) => { return 'Hello ' + name; }; const sayGoodbye = (name) => { return 'Goodbye ' + name; } export default sayHello;La fonction
sayGoodbye("John")invoquée dans le fichierapp.jsgénère une erreur. Elle n'a pas été exportée ni importée et appartient pour le moment uniquement au scope du fichiergreet.js.shHello John ❌ Uncaught ReferenceError: sayGoodbye is not definedDe cette manière, les fichiers peuvent coexister en toute sécurité.
Nous allons voir qu'en exploitant cette fonctionnalité, AssetMapper va nous donner la possibilité de structurer notre code JavaScript qui sera composé de fonctions ou de classes réutilisables et isolées les unes des autres. Et tout ceci nativement dans les navigateurs, sans utiliser de bundler 🥳.
La courte vidéo ci-dessous explique tout ceci pas à pas avec une classe en exemple. ☝️ MUST WATCH 👇
↗https://symfonycasts.com/screencast/asset-mapper/es6-browser#playDoing Modern JS Right in your Browser Explication en vidéo (7 minutes) de Ryan Weaver - contributeur majeur de Symfony et SymfonyCast.
symfonycasts.com
AssetMapper ready
Si tu as installé une version 6.3 de Symfony ou supérieure, ce qui est probablement le cas, tu n'as rien à faire, AssetMapper est déjà installé et préconfiguré. Mais pour information, si tu dois l'installer sur un projet, voici la commande à lancer avec composer.
composer require symfony/asset-mapper symfony/asset symfony/twig-pack
Comme pour d'autres bundles, SymfonyFlex exécute quelques recettes après le téléchargement des dépendances. Pour AssetMapper, cela concerne les fichiers ci-après. Ouvre-les avec ton IDE et regardons de quoi il s'agit.
- 1
config/packages/asset_mapper.yaml
Un fichier de configuration YAML où est défini le chemin vers le dossier de tes
assets. - 2
importmap.php
À la racine de ton projet, ce fichier sera automatiquement mis à jour à l'installation de chaque nouvelle dépendance. C'est également ici que tu pourras définir les points d'entrée de ton application. 💡 point d'entrée : nous y voilà 😎
php//importmap.php return [ 'app' => [ 'path' => './assets/app.js', 'entrypoint' => true, ],Oui il s'agit bien d'un tableau PHP et son rôle est de permettre à AssetMapper de générer les balises HTML
<link>et<script>. Et si tu regardes le fichierbase.html.twig, tu remarques que SymfonyFlex a également écrit ceci :{{ importmap('app') }}. C'est ce helper Twig qui fera le lien. - 3
assets/app.js
Le fichier JavaScript principal, ce fameux point d'entrée, dans lequel tu pourras importer d'autres fichiers. Il contient d'ailleurs déjà deux imports.
jsimport './bootstrap.js'; import './styles/app.css';- Le premier
./bootstrap.js: ⚠️ il ne s'agit pas du framework frontend du même nom que tu connais sans doute déjà. Il s'agit d'un fichier lié à la bibliothèque Stimulus dont nous parlerons dans une prochaine quête. Ne supprime pas cette ligne mais n'y prête pas attention pour le moment. - Le second
./styles/app.css: une référence au fichierapp.cssgénéré lui aussi par SymfonyFlex au moment de l'installation et dont tu peux te servir pour définir tes règles de style. C'est d'ailleurs grâce à ce fichier que tu as une page sur fond bleu 😎.
InfoÀ noter : l'import de fichiers CSS dans un fichier JavaScript est d'ordinaire impossible. Si tu essaies de le faire dans contexte classique, tu obtiendras l'erreur suivante dans la console de ton navigateur :
❌ Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/css". Strict MIME type checking is enforced for module scripts per HTML spec.
Cela fonctionnerait indirectement avec un bundler comme Webpack ou Vite car il injecterait un lien vers la feuille de style dans la page HTML. Ici c'est AssetMapper qui se charge d'ajouter ce lien vers le fichier
app.cssau DOM. D'ailleurs, si tu inspectes le code source de ta page d'accueil, tu trouveras cette ligne dans la partie<head>(lesxxxxxétant remplacés par une série de chiffres et de lettres) :html<link rel="stylesheet" href="/assets/styles/app-xxxxxxxxx.css">Consulte cette autre vidéo de Ryan Weaver à 1:16:06 si tu veux comprendre comment cela marche.
- Le premier
Les images de décoration
Pour faire référence à tes images de décoration (pas les images de contenu qui, elles, seront gérées autrement), tu vas pouvoir utiliser le helper Twig asset(). Voici par exemple comment afficher l'image duck.png qui serait située dans le dossier assets/images.
<img src="{{ asset('images/duck.png') }}" alt="" />
Plus d'infos concernant les chemins vers des fichiers au sein d'un CSS : Paths Inside of CSS Files
À noter : il faut indiquer le chemin complet vers le fichier en question DEPUIS le dossier assets.
Premières conclusions
Et voilà, tu sais à peu près tout d'AssetMapper !
Un fichier importmap.php qui liste les points d'entrée de ton application qui, eux, importent tes fichiers JavaScript et CSS. Et ce fichier importmap.php se charge de générer les balises HTML <link> et <script> ainsi que les fichiers finaux dont les noms seront versionnés (app-xxxxxxxxx.css, app-xxxxxxxxx.js, etc.).
La ressource suivante te donne un exemple d'utilisation.
Écrire du JavaScript avec AssetMapper.
Mais alors, il n'y a rien à faire de plus ? Laisse-moi réfléchir 🤔.
Non 😀, c'est parti tu peux coder 🚀.
Ah si, nous allons parler du cas où tu souhaites installer d'autres dépendances. Mais d'abord, révisons un peu tout ça.
🎯 Quiz
false|||false|||true
# AssetMapper repose sur les modules ES6
[] Et génère l'appel vers le fichier principal **app.js** avec le code HTML :
```html
<script src="app.js"></script>
```
[x] Et génère l'import du point d'entrée **app.js** avec le code HTML :
```html
<script type="module">import 'app';</script>
```
# En utilisant AssetMapper, quelle écriture me permet d'importer la fonction hello() du fichier myAwesomeScript.js dans mon fichier app.js ?
[]
```js
const myAwesomeScript = require('myAwesomeScript.js');
myAwesomeScript.hello();
```
[x]
```js
import hello from 'myAwesomeScript.js';
```
[]
```css
@import 'myAwesomeScript.js';
hello();
```
# AssetMapper ajoute une série de chiffres et de lettres aux noms des fichiers app.js et app.css à chaque fois qu'ils sont modifiés pour générer des noms uniques.
[x] En procédant ansi, les navigateurs forceront le téléchargement des nouvelles versions au lieu d'utiliser les versions mises en cache. On dit alors que les noms sont versionnés.
[] Cela permet de renforcer la sécurité en brouillant les noms des fichiers.
[] Cette contrainte est dictée par Git.
Installer des dépendances
Tu as sans doute remarqué un autre dossier dans le dossier assets. Le dossier vendor. Étrange 🤔. Que fait donc ce dossier ici ?
Je te rassure, il ne s'agit pas d'une erreur de manipulation. Ce dossier est bien à sa place, et comme celui du même nom en racine, il n'est pas versionné avec Git. Tu le devines sans doute, ce dossier va héberger tes dépendances frontend.
- 1
De Webpack Encore à AssetMapper
Avant l'arrivée d'AssetMapper, Symfony préconisait l'utilisation de Node et du gestionnaire Yarn pour gérer les dépendances. Le builder Webpack était alors configuré (sous le bundle Webpack Encore) pour manipuler, compiler, transpiler, minifier… nos assets et rendre nos fichiers JavaScript compatibles avec tous les navigateurs. Toutes les dépendances frontend d'un projet étaient alors stockées dans un dossier
node_modules.Mais ça c'était avant !
Aujourd'hui AssetMapper propose de se connecter à NPMJS pour télécharger toutes les dépendances dont tu as besoin dans le fameux dossier
vendordu dossierassets. Et cela va pouvoir se faire grâce à Symfony CLI et la commande :bashphp bin/console importmap:require nom_du_packageInfoPour trouver le nom exact d'un package, rends-toi sur NPMJS et recherche la dépendance qui t'intéresse. Lorsque tu suis le lien, tu obtiens le nom en début de page ou via la commande
npm iqui est proposée. Voici quelques exemples :- Pour Pico CSS c'est
@picocss/pico - Bootstrap :
bootstrap - Uikit :
uikit - etc.
- Pour Pico CSS c'est
- 2
Installer Bootstrap
Entrons dans le vif du sujet et installons Bootstrap, le célèbre framework frontend cette fois-ci.
Lance la commande suivante :
bashphp bin/console importmap:require bootstrapLe téléchargement est très rapide et lorsqu'il est terminé, la console t'indique que trois entrées ont été ajoutées au tableau du fichier
importmap.php.phpreturn [ //... 'bootstrap' => [ 'version' => '5.3.3', ], '@popperjs/core' => [ 'version' => '2.11.8', ], 'bootstrap/dist/css/bootstrap.min.css' => [ 'version' => '5.3.3', 'type' => 'css', ], ];Nous n'en avons pas parlé tout à l'heure, mais côté HTML une des balises
<script>qui est générée possède l'attributtype="importmap". Regardons son contenu.html<script type="importmap" data-turbo-track="reload"> { "imports": { "app": "/assets/app-dc80e6c89135215834fae357c55df6c3.js", "/assets/bootstrap.js": "/assets/bootstrap-c423b8bbc1f9cae218c105ca8ca9f767.js", "/assets/styles/app.css": "data:application/javascript,", "@symfony/stimulus-bundle": "/assets/@symfony/stimulus-bundle/loader-e1ee9ace0562f2e6a52301e4ccc8627d.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", "bootstrap": "/assets/vendor/bootstrap/bootstrap.index-c0423c99f6075e6b1cef7579b5c00d32.js", "@popperjs/core": "/assets/vendor/@popperjs/core/core.index-ceb5b6c0f9e1d3f6c78ef733facfdcda.js", "bootstrap/dist/css/bootstrap.min.css": "data:application/javascript,document.head.appendChild%28Object.assign%28document.createElement%28%22link%22%29%2C%7Brel%3A%22stylesheet%22%2Chref%3A%22%2Fassets%2Fvendor%2Fbootstrap%2Fdist%2Fcss%2Fbootstrap.min-1712f0378f8675ca7cd423d6262fcccf.css%22%7D%29%29" } } </script>Tu y retrouves à peu près tous les index du tableau
importmap.phpdont ton point d'entréeapp.jsmais également ta dépendance Bootstrap fraîchement installée. Ce qu'il faut comprendre ici c'est que tu vas pouvoir te servir des index du tableau, et notammentbootstrapetbootstrap/dist/css/bootstrap.min.csspour y faire référence dans tes fichiers JavaScript et pouvoir ainsi charger les fichiers associés. Il seront automatiquement « mappés » par le navigateur.Ouvre ton fichier
assets/app.jset ajoutes-y les lignes suivantes :jsimport 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap';À présent rends-toi sur la documentation de Bootstrap, récupère le code d'un composant et intègre-le à ta page. Tu devrais voir apparaître ton composant fonctionnel.
- 3
Exemple avec une Navbar
Voici ce que tu pourrais mettre en place pour afficher une Navbar Bootstrap (avec dropdown et menu burger responsive)
jsimport './bootstrap.js'; /* * Welcome to your app's main JavaScript file! * * This file will be included onto the page via the importmap() Twig function, * which should already be in your base.html.twig. */ import './styles/app.css'; import 'bootstrap/dist/css/bootstrap.min.css'; import 'bootstrap';html<nav class="navbar navbar-expand-lg bg-body-tertiary"> <div class="container-fluid"> <a class="navbar-brand" href="#">Navbar</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarSupportedContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <a class="nav-link active" aria-current="page" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">Link</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false"> Dropdown </a> <ul class="dropdown-menu"> <li><a class="dropdown-item" href="#">Action</a></li> <li><a class="dropdown-item" href="#">Another action</a></li> <li><hr class="dropdown-divider"></li> <li><a class="dropdown-item" href="#">Something else here</a></li> </ul> </li> <li class="nav-item"> <a class="nav-link disabled" aria-disabled="true">Disabled</a> </li> </ul> <form class="d-flex" role="search"> <input class="form-control me-2" type="search" placeholder="Search" aria-label="Search"> <button class="btn btn-outline-success" type="submit">Search</button> </form> </div> </div> </nav>twig<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{% block title %}Welcome!{% endblock %}</title> {% block stylesheets %} {% endblock %} {% block javascripts %} {% block importmap %}{{ importmap('app') }}{% endblock %} {% endblock %} </head> <body> {% include '_navbar.html.twig' %} {% block body %}{% endblock %} </body> </html>Plutôt simple non ?
- 4
Désinstaller une dépendance
Tu as vu que la bibliothèque
@popperjs/coredont dépend Bootstrap avait été automatiquement téléchargée par AssetMapper. Cette fonctionnalité du bundle est très pratique pour être sûr de ne rien oublier. En revanche pour désinstaller une dépendance, il te faudra lister l'ensemble des entrées ajoutées en utilisant la commandephp bin/console importmap:remove nom_du_package1 package2 etc.. Voici par exemple la commande pour désinstaller bootstrap.bashphp bin/console importmap:remove bootstrap @popperjs/core bootstrap/dist/css/bootstrap.min.cssTravailler avec des fichiers SCSS
Petit rappel sur ce qu'est le SCSS avec la quête suivante.
Quêtes liéesTravailler avec des fichiers Sass (scss) était une fonctionnalité très utilisée dans un contexte de builder comme Webpack. Cela reste possible avec AssetMapper grâce au sass-bundle de SymfonyCasts.
Pour l'installer, comme d'habitude avec composer, lance la commande suivante :
bashcomposer require symfonycasts/sass-bundleRenomme ensuite ton fichier
assets/app.cssen =>app.scss. Pense également à mettre à jour le fichierapp.jsjsimport './styles/app.scss';Lorsque tu recharges ta page, tu as cette erreur :
ImportantAn exception has been thrown during the rendering of a template […]/var/sass/app.output.css doesn't exist
Eh oui, il faut lancer la commande de build pour demander au bundle sass-bundle de convertir tes fichiers Sass en fichier CSS :
bashphp bin/console sass:build --watchLorsque tu recharges ton navigateur, tout fonctionne correctement et si tu effectues des modifications de style, ils sont bien pris en compte.
AttentionTrès important si tu utilises sass-bundle pour faire du SCSS Pour être sûr de ne pas oublier de lancer la commande de build à chaque fois que tu démarres ton projet, tu peux ajouter un fichier
.symfony.local.yamlen racine de ton dossier avec ce qui suit à l'intérieur. Cela aura pour effet de démarrer automatiquement le build à chaquesymfony server:startet chaque fois que tu modifies tes fichiers.yamlworkers: # ... sass: cmd: ['symfony', 'console', 'sass:build', '--watch']Plus d'info ici https://symfony.com/bundles/SassBundle/current/index.html#symfony-cli
Si tu utilises du SCSS c'est probablement pour travailler avec les fichiers d'origine d'un framework.
Reprenons l'exemple de Bootstrap que tu as installé tout à l'heure. Si tu te souviens bien, tu faisais appel au fichier CSS minifié
bootstrap.min.csstéléchargée depuis NPMJS. Cette feuille de style ne te permet pas de changer le thème facilement. Pour cela, tu vas récupérer les fichiers SCSS du framework ce qui te permettra de changer toutes les variables selon ton design. Cette fois-ci, c'est composer qui va être utilisé et qui va télécharger les fichiers source depuis Packagist.Lance cette commande :
bashcomposer require twbs/bootstrapTu peux maintenant y faire référence et l'importer dans ton fichier
app.scsscomme ceci :scss@import '../../vendor/twbs/bootstrap/scss/bootstrap';Enfin, retire l'appel au fichier minifié
bootstrap.min.cssdepuis le point d'entréeapp.jspuisqu'il ne sert plus à rien.Retrouve toute la marche à suivre sur la documentation du bundle Sass-bundle.
Pour changer la couleur primary, tu peux par exemple procéder comme ceci :
scss//app.scss $primary: #8302ab; @import '../../vendor/twbs/bootstrap/scss/bootstrap';Retrouve les recommandations complètes à la page Sass de la documentation de Bootstrap.
InfoSi tu souhaites installer un autre framework que Bootstrap et utiliser les fichiers source SCSS, il te faudra aussi les télécharger depuis Packagist avec le nom exact du package. Utilise la barre de recherche comme tu l'as fait sur NPMJS. Voici quelques exemples :
- Pour Pico CSS c'est
composer require picocss/pico - Bootstrap :
composer require twbs/bootstrap - Uikit :
composer require uikit/uikit - etc.
Consulte ensuite les documentations respectives pour importer les fichiers dans ton fichier d'entrée
app.scss. - Pour Pico CSS c'est
En résumé
Pour gérer les assets d'un projet web avec AssetMapper dans Symfony :
- Utiliser le point d'entrée
app.jspour importer tes autres fichiers JavaScript en mode ES6 modules ainsi que tes dépendances. - Utiliser le fichier
app.cssqui sera également importé dans le point d'entrée. - Si tu souhaites utiliser des dépendances frontend, lancer la commande :Il faudra ensuite récupérer via le tableau du fichierbash
php bin/console importmap:require nom_de_la_dépendanceimportmap.phple nom de l'index à importer dans ton fichier JavaScriptapp.js. - Si tu souhaites travailler avec du SCSS, il faudra installer le bundle sass-bundle de SymfonyCasts et lancer la commande
php bin/console sass:build --watchà chaque fois que tu démarres ton projet. Il faudra également télécharger les fichiers source SCSS depuis Packagist avec Composer et la commandecomposer require vendor_name/package_namepuis les importer dans ton fichier SCSS.
Mise en production
Une dernière chose qui ne te concerne pas tout de suite mais que tu dois savoir en tant que développeur. Tout ce que tu as vu dans cette quête est valable en mode développement. Si tu mets ton projet en ligne, tu passeras en mode production. Il faudra alors lancer la commande suivante pour copier tout le contenu du dossier assets dans le dossier public :
php bin/console asset-map:compile