đ 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 :
La magnifique dĂ©mo de cet article : đ https://labs.silarhi.fr/images
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.

Top comments (0)