Gérer ses miniatures avec Symfony et Glide
Utilisez la librairie PHP Glide pour gérer vos miniatures de chaton comme un pro avec Symfony !
🎉 Cet article a été cité dans le Week of Symfony #702 🎉
Aujourd'hui j'ai envie de vous parler d'un pattern récurrent dans un projet Web : la gestion des images et surtout des miniatures associées.
En 2021, le poids des images dans une page Web est toujours un facteur clé dans l'optimisation du poids des pages. Combien d'entre-nous continue encore de servir aux internautes mobiles des images de 1200px de long ?
On va donc voir comment mettre en place dans un projet Symfony 5 la librairie PHP Glide qui met à disposition une API pour gérer la manipulation d'image.
Démo
Avant de partir dans les explications, je vais déjà tenter de vous attendrir avec le type de rendu final que l'on peut obtenir avec cette librairie :
Installation
Sans plus attendre on va commencer par installer tout doucement la librairie et créer un service Symfony pour gérer tout ça. Le package league/glide-symfony
possède un adapter
pour Symfony qui intègre la gestion des Response
mais ne fournit pas de service par défaut.
composer require league/glide league/glide-symfony
Configuration
On va modifier un peu la configuration actuelle pour ajouter le service Symfony et préparer l'API.
# config/services.yaml
parameters:
app.public_dir: '%kernel.project_dir%/public'
app.image_cache_dir: '%kernel.project_dir%/var/storage/cache'
services:
_defaults:
#...
bind:
$secret: '%kernel.secret%'
League\Glide\Server:
factory: ['League\Glide\ServerFactory', create]
arguments:
source: '%app.public_dir%'
cache: '%app.image_cache_dir%'
Mise en place
L'idée est ensuite de créer un Controller
pour générer à la volée les miniatures (c'est notre "API"). On passera dans la requête le chemin relatif de l'image à utiliser ainsi que des pour indiquer la longueur / largeur désirée de la miniature, la couleur de fond, etc ...
Note sur la sécurité : Pour empêcher des utilisateurs malveillants de demander à générer sur votre serveur des centaines de miniatures aux dimensions non voulues (Imaginez votre serveur devant répondre à 10 000 requêtes de miniatures au format 50000x50000), nous allons également ajouter un hash de signature qui sera unique pour les dimensions données et que le Controller
devra valider pour générer la miniature.
Le controller
<?php
//src/Controller/AssetsController.php
namespace App\Controller;
use League\Glide\Filesystem\FileNotFoundException;
use League\Glide\Responses\SymfonyResponseFactory;
use League\Glide\Server;
use League\Glide\Signatures\SignatureException;
use League\Glide\Signatures\SignatureFactory;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController as Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class AssetsController extends Controller
{
/**
* @Route("/assets/{path<(.+)>}", name="asset_url", methods={"GET"})
*/
public function asset(string $path, string $secret, Request $request, Server $glide)
{
$parameters = $request->query->all();
if (\count($parameters) > 0) {
try {
SignatureFactory::create($secret)->validateRequest($path, $parameters);
} catch (SignatureException $e) {
throw $this->createNotFoundException('', $e);
}
}
$glide->setResponseFactory(new SymfonyResponseFactory($request));
try {
$response = $glide->getImageResponse($path, $parameters);
} catch (\InvalidArgumentException | FileNotFoundException $e) {
throw $this->createNotFoundException('', $e);
}
return $response;
}
}
L'extension Twig
<?php
// src/Twig/AssetExtension.php
namespace App\Twig;
use League\Glide\Signatures\SignatureFactory;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
class AssetExtension extends AbstractExtension
{
/** @var UrlGeneratorInterface */
private $router;
/** @var string */
private $secret;
public function __construct(UrlGeneratorInterface $router, string $secret)
{
$this->router = $router;
$this->secret = $secret;
}
public function getFunctions()
{
return [
new TwigFunction('app_asset', [$this, 'appAsset'], ['is_safe' => ['html']]),
];
}
public function appAsset(string $path, array $parameters = [], $referenceType = UrlGeneratorInterface::ABSOLUTE_PATH)
{
$parameters['fm'] = 'pjpg';
if ('png' === substr($path, -3)) {
$parameters['fm'] = 'png';
}
$parameters['s'] = SignatureFactory::create($this->secret)->generateSignature($path, $parameters);
$parameters['path'] = ltrim($path, '/');
return $this->router->generate('asset_url', $parameters, $referenceType);
}
}
Avec ces 2 nouveaux fichiers, vous avez un système tout prêt de génération de miniatures sécurisé pour votre belle application !
Utilisation
Il ne vous reste plus qu'à utiliser votre fonction twig dans vos templates. La version minimale pourrait ressembler à ceci :
{# templates/images.html.twig #}
<img class="img-fluid"
src="{{ app_asset(asset('images/mon-image.jpg'), {'w': 400, 'h': 200}) }}"
width="400"
height="200"
alt="Ma super image de 400x200"
title="Ma super image de 400x200"
/>
Ce qui produit l'URL suivante :
/assets/images/mon-image.jpg?w=400&h=200&fm=pjpg&s=d6ba0d9ef7622516b83fa3402527affe
Ce qui revient à demander au serveur :
Génère-moi la miniature du fichier
/images/mon-image.jpg
(si elle n'est pas déjà générée) d'une longueur de 400px (paramètrew
), d'une hauteur de 200px (paramètreh
), au format JPG Progressive (paramètrefm
). Pour te prouver que je suis de bonne foi, je te donne le hash de vérification de ces 4 paramètres (paramètres
). Merci bisous.
Gestion des écrans retina
La gestion des écrans retina devient beaucoup plus simple vu qu'il n'y a plus qu'à passer les bons paramètres au contrôleur.
{# templates/images.html.twig #}
<img class="img-fluid"
src="{{ app_asset(asset('images/mon-image.jpg'), {'w': 400, 'h': 200}) }}"
srcset="{{ app_asset(asset('images/mon-image.jpg'), {'w': 400, 'h': 200, 'dpr': 2}) }}" 2x
width="400"
height="200"
alt="Ma super image de 400x200"
title="Ma super image de 400x200"
/>
J'ai juste ajouté avec le paramètre dpr
(Device pixel ratio) pour générer une image 2 fois plus dense (en l'occurence 800x400).
Aller plus loin
Le bon développeur ou la bonne développeuse que vous êtes a sûrement détecté un pattern à factoriser pour améliorer la maintenabilité de tout ça. Je vous joins les 2 macros que j'utilise dans quasiment tous mes projets Symfony :
{# templates/macros.html.twig #}
{% macro image(path, width, height, attrs, params) %}
<img src="{{ app_asset(path, params|default({})|merge({'w': width, 'h': height})) }}"
srcset="{{ app_asset(path, params|default({})|merge({'w': width, 'h': height, 'dpr': 2})) }} 2x"
width="{{ width }}"
height="{{ height }}"
loading="lazy"
{% for name,value in attrs|default([])|filter((k, v) => k != 'class') %}{{ name }}="{{ value|e }}" {% endfor %}
/>
{% endmacro %}
{% macro fixedHeightImage(path, height, attrs) %}
<img class="img-fluid {{ attrs.class|default(null) }}"
src="{{ app_asset(path, {'h': height}) }}"
srcset="{{ app_asset(path, {'h': height, 'dpr': 2}) }} 2x"
loading="lazy"
{% for name,value in attrs|default([]) |filter((k, v) => k != 'class') %}{{ name }}="{{ value|e }}" {% endfor %}
/>
{% endmacro %}
{# templates/images.html #}
{% import 'macros.html.twig' as macros %}
{{ macros.image(
asset('images/mon-image.jpg'),
300,
300,
{'class': 'img-fluid'},
) }}
Voilà ! Vous êtes maintenant un pro des miniatures ! N'hésitez pas à regarder le code source de la démo qui intègre Webpack en plus de tout ça.