Supprimer Webpack, … de vos projets Symfony ?

J’ai personnellement beaucoup de mal avec les outils Node.JS comme Webpack, Grunt, Gulp, Yarn, NPM, etc. Ils peuvent être nécessaire pour une application front (Angular, React, VueJS, etc) mais sont parfois inutiles pour des applications web PHP classique (Ex : Symfony + Twig).

Au-delà de leur utilité discutable, l’écosystème Node.js évolue rapidement. A chaque fois que vous revenez sur un projet, après quelques mois, vous devez consacrer du temps à mettre à jour les outils devenus obsolètes.

De plus, les configurations de ces outils sont rarement maîtrisées (👋Webpack) et une fois qu’une configuration est en place, rares sont les développeurs et développeuses qui s’aventurent à essayer de comprendre et d’adapter ce qui est déjà en place.
Qui n’a jamais perdu des heures 👀 à installer un projet à cause de ce genre d’outils ? Ou tout simplement parce que vous n’avez pas la bonne version de NodeJS ?

Enfin, ce sont des outils qui tournent en local sur l’ordinateur (ou WSL ou docker). Dans certains cas, vous pouvez avoir des temps de build assez pénible, des tâches de surveillance (watch) qui plantent sournoisement et même des consommations excessives de ressources.

Avez-vous vraiment besoin de Webpack & Co ? 🤷

Tout dépend du projet mais si vous travaillez sur un projet PHP (Laravel / Symfony / Laminas) avec assez peu de dépendance JavaScript, vous n’avez probablement pas besoin de Webpack (ou gulp, ou autre). Si vous ne savez même pas ce que fait Webpack, c’est probablement qu’il est là par défaut et que vous pourriez sûrement vous en passer.

Soyons clair, Si vous avez besoin de Webpack & Co utilisez-les ! Mais sinon, simplifiez vous la vie (et celle de vos collègues) en supprimant Node.js de vos projets

Pourquoi chercher en Node.js ce qui se trouve en 🐘 PHP ?

Oublions les outils et revenons sur les problématiques que nous cherchons à résoudre.
Voyons de quoi nous avons besoin :

  • Transpiler des fichiers SASS ou LESS en CSS ?
  • Concaténer des fichiers CSS ?
  • Minifier des fichiers CSS ?
  • Concaténer des fichiers JavaScript ?
  • Minifier des fichiers JavaScript ?
  • Mettre en cache navigateur les fichiers de production ?
  • Généré à la volé les fichiers en environnement de développement ?

D’après vous, est-ce possible de faire cela avec le meilleur langage au monde : PHP ? 😁

Utilisez des routes pour générer votre CSS et JS

Pour versionner les fichiers CSS et JS, nous allons utiliser le versionning de l’application.

Indiquez le numéro de version dans le fichier .env.

# .env
APP_VERSION=1.0.0

Déclarez le numéro de version en paramètre dans config/services.yaml

# config/services.yaml
parameters:
    app.version: '%env(APP_VERSION)%'

Créez un nouveau contrôleur src/controllers/assetsController.php avec les routes suivantes :

  • /css/{version}.css
  • /js/{version}.css

Le numéro de version est récupéré automatiquement depuis la configuration.

<?php
// src/controllers/AssetsController.php

namespace App\Controller;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;


#[Route(path: '/assets', name: 'asset.', requirements: ['version' => '[0-9.]+'], defaults: ['version' => '%app.version%'])]
class AssetsController extends AbstractController
{
    #[Route('/{version}.css', name: 'css')]
    public function css(string $version): Response
    {
        return new Response('body{background:red}',
            Response::HTTP_OK, ['Content-Type' => 'text/css']
        );
    }

    #[Route('/{version}.js', name: 'js')]
    public function js(string $version): Response
    {
        return new Response('console.log("loaded")',
            Response::HTTP_OK, ['Content-Type' => 'text/javascript']
        );
    }
}

Afin de vérifier que tout fonctionne, testez les deux routes en naviguant sur votre projet :

  • /assets/1.0.0.js
  • /assets/1.0.0.css

Créez un service Symfony pour générer votre CSS

Utilisez composer pour installer les deux dépendances suivantes :

composer req scssphp/scssphp
composer req matthiasmullie/minify

Déclarez le chemin de vos fichiers SCSS dans la configuration :

services:
    App\Service\CssService:
        arguments:
            $config: {
                path: '../assets/css',
                pattern: '*.scss'
            }

    ScssPhp\ScssPhp\Compiler:
        class: 'ScssPhp\ScssPhp\Compiler'

    MatthiasMullie\Minify\CSS:
        class: 'MatthiasMullie\Minify\CSS'

Créez un service CssService permettant de builder le css.

<?php
// src/Service/CssService.php

namespace App\Service;

use MatthiasMullie\Minify\CSS;
use ScssPhp\ScssPhp\Compiler;

class CssService
{
    protected array $config;
    protected Compiler $scssCompiler;
    protected CSS $cssMinifier;

    public function __construct(array $config, Compiler $scssCompiler, CSS $cssMinifier)
    {
        $this->config       = $config;
        $this->scssCompiler = $scssCompiler;
        $this->cssMinifier  = $cssMinifier;
    }

    public function build(string $version): string
    {
        $content = '';

        // Fusionne tous les fichiers CSS
        $pattern = $this->config['path'] . $this->config['pattern'];

        foreach (glob($pattern, GLOB_BRACE) as $css) {
            $content .= file_get_contents($css);
        }

        // Compile le contenu avec SASS/SCSS
        $content = $this->scssCompiler->compileString($content)->getCss();

        // Allège le fichier en le minifiant
        $content = $this->cssMinifier->add($content)->minify();

        // Ajoute un entête au fichier CSS
        $dateTime = date('Y-m-d H:i:s');

        return "/* CSS Version $version - $dateTime */" . PHP_EOL . $content;
    }
}

Ajoutons ce service dans notre contrôleur AssetsController.php

// ...

    #[Route('/{version}.css', name: 'css')]
    public function css(CssService $cssService, string $version): Response
    {
        $css = $cssService->build($version);

        return new Response($css, Response::HTTP_OK, ['Content-Type' => 'text/css']);
    }

// ...

Ajoutez des fichiers .scss dans le dossier /assets/css et testez ! 😎

Créez un service Symfony pour générer votre JS

Déclarez le chemin de vos fichiers JS dans la configuration :

services:
    App\Service\JsService:
        arguments:
            $config: {
                path: '../assets/js',
                pattern: '*.js'
            }

    MatthiasMullie\Minify\JS:
        class: 'MatthiasMullie\Minify\JS'

Créez un service JsService permettant de builder le js.

<?php
// src/Service/JsService.php

namespace App\Service;

use MatthiasMullie\Minify\JS;

class JsService
{
    protected array $config;
    protected JS $jsMinifier;

    public function __construct(array $config, JS $jsMinifier)
    {
        $this->config     = $config;
        $this->jsMinifier = $jsMinifier;
    }

    public function build(string $version): string
    {
        $content = '';

        // Fusionne tous les fichiers Javascript
        $pattern = $this->config['path'] . $this->config['pattern'];
        foreach (glob($pattern, GLOB_BRACE) as $js) {
            $content .= file_get_contents($js);
        }

        // Allège le fichier en le minifiant
        $content = $this->jsMinifier->add($content)->minify();

        // Ajoute un entête au fichier JS
        $dateTime = date('Y-m-d H:i:s');

        return "/* JS Version $version - $dateTime */" . PHP_EOL . $content;
    }
}

Ajoutons ce service dans notre contrôleur AssetsController.php

// ...

    #[Route('/{version}.js', name: 'js')]
    public function js(JsService $jsService, string $version): Response
    {
        $js = $jsService->build($version);

        return new Response($js, Response::HTTP_OK, ['Content-Type' => 'text/javascript']);
    }

// ...

Ajoutez des fichiers .js dans le dossier /assets/js et testez ! 😎

C’est génial ! Comment utiliser ce système maintenant ?

Utilisez vos routes CSS et JS dans votre template Twig :

<!DOCTYPE html>
<html lang="fr">
<head>
    <!-- ... -->
    <link rel="stylesheet" href="{{ path('asset.css') }}">
</head>
<body>

    <!-- ... -->
    <script async src="{{ path('asset.js') }}"></script>

</body>
</html>

Organisez vos fichiers SCSS et JS comme bon vous semble. Il suffit de modifier la configuration du fichier services.yaml pour adapter cette méthode à votre architecture.

Si vous souhaitez que certains fichiers se chargent avant d’autres, il vous suffit de les préfixer par un numéro. Par exemple :

Et pour la production ? Comment éviter que le build se fasse à chaque fois ?

Même si la génération à la volée du CSS et du JS peut vous sembler instantanée, il est bon de ne pas faire ce traitement à chaque visiteur. Voyons comment mettre en cache de manière astucieuse.

Astuce : Notre serveur web est configuré pour envoyer des fichiers statiques s’ils existent. C’est uniquement si le fichier n’existe pas que le serveur web contact notre application Symfony, exécute la route et retourne la réponse. Utilisons ce mécanisme pour gérer efficacement notre cache.

Pour faire simple, si le fichier /assets/1.0.0.css existe sur le serveur, il sera renvoyé. Sinon, la route Symfony /assets/1.0.0.css sera appelée. C’est cool non ? 😉

Et la 🍒 sur le 🍰. Il n’y aura jamais de problème de cache navigateur car le navigateur ira systématiquement chercher la version 1.0.0.css. Puis au prochain déploiement, il ira chercher la version 1.1.0.css.

Voyons comment générer ce fichier statique à la volé, uniquement en environnement de production :

// ...

    public function __construct(private Kernel $kernel)
    {
    }

// ...

    #[Route('/{version}.css', name: 'css')]
    public function css(CssService $cssService, string $version): Response
    {
        $css = $cssService->build($version);

        if ($this->kernel->getEnvironment() === 'prod') {
            // Mise en cache : Fichier servi par le serveur web directement
            file_put_contents($this->kernel->getProjectDir() . '/public/assets/' . $version . '.css', $css);
        }

        return new Response($css, Response::HTTP_OK, ['Content-Type' => 'text/css']);
    }

// ...

    #[Route('/{version}.js', name: 'js')]
    public function js(JsService $jsService, string $version): Response
    {
        $js = $jsService->build($version);

        if ($this->kernel->getEnvironment() === 'prod') {
            // Mise en cache : Fichier servi par le serveur web directement
            file_put_contents($this->kernel->getProjectDir() . '/public/assets/' . $version . '.js', $js);
        }

        return new Response($js, Response::HTTP_OK, ['Content-Type' => 'text/javascript']);
    }

// ...

Alors, prêt à vous passer de Node.js sur votre prochain projet PHP ? 😉

Après avoir utilisé cette technique sur 6 projets et avec plusieurs équipes, j’ai remarqué plusieurs avantages :

  • Le temps d’installation du projet par une nouvelle développeuse ou un nouveau développeur est beaucoup plus rapide
  • Les projets sont plus facile à maintenir dans le temps car on évite les problèmes de version de Node.JS et warning/deprecated NPM
  • Le confort de développement est augmenté car il n’est plus nécessaire d’attendre un watch pour voir le résultat
  • Les équipes s’autorisent plus facilement du refactoring d’architecture car la configuration est maitrisée par tous.

Pour ce qui est des inconvénients, il y en a surement mais personnellement, pour mon usage, je n’en ai aucun pour l’instant.

N’hésitez pas à tester et à venir en discuter sur Twitter.

Ressources récentes