Optimisation des images : focus sur VichUploader et LiipImagine
Lorsqu'on développe une application web, une bonne gestion des images est primordiale afin d'améliorer l'expérience utilisateur. En effet, les images rendent la page plus attrayante mais ont néanmoins une influence directe sur le temps d’affichage. Il est donc important de trouver le bon compromis. Des images trop optimisées pour le temps de chargement peuvent voir leur qualité fortement dégradée. Au contraire, des images dont la taille n'a pas été assez optimisée peuvent impacter le temps de chargement, notamment si elles sont nombreuses.
Cet article vous propose une solution sous Symfony avec des bundles permettant de pouvoir automatiser le rangement de ces images mais aussi de les perfectionner avec de nombreux paramètres.
VichUploaderBundle pour la gestion de l’upload de fichier
VichUploaderBundle propose une façon simple de sauvegarder un fichier téléchargé dans des entités d’ORM, documents MongoDB ou encore des modèles Propel.
Parmi ses fonctionnalités, on peut noter qu’il :
- peut être lié à différents types de moteurs (doctrine, mongoDB, Propel, PHPCR)
- permet de mettre en place un processus de naming automatique des fichiers et répertoires
- fournit des ‘form type’ pour les images ou fichiers
- permet d’utiliser une variété de filesystem abstraction layer (Gaufrette, Flysystem ou personnalisé)
- met à disposition des ‘Events’ à l’upload ou à la suppression
LiipImagineBundle pour la retouche d’image
LiipImagineBundle fournit de nombreux filtres à appliquer sur des images. Il génère les images avec les filtres définis à l’affichage et non à l’upload par défaut.
Parmi ses fonctionnalités, on retrouve :
- de nombreux filtres disponibles : thumbnail, scale, crop, flip, strip et watermark
- des post processors pour les formats JPG et PNG, personnalisables pour gérer davantage de formats.
- différents Loader pour les systèmes de fichiers, par exemple : File System, FlySystem, GridFS, Stream…
- gestion du cache compatible avec différents services : Amazon S3, AWS S3, Cache, FlySystem…
Exemple d’implémentation de récupération d'image
Dans la suite de l’article, nous vous proposons un exemple d’implémentation illustré mais non complet. Nous commencerons avec une configuration de ces deux bundles puis une logique de récupération de chemin d’image.
Mise en place entité
Dans notre exemple, l’entité UserPicture sera liée à un utilisateur et correspondra à l’avatar.
<?php
namespace AppBundle\Doctrine\Entity;
use Ramsey\Uuid\Uuid;
use Symfony\Component\HttpFoundation\File\File;
use AppBundle\User\BaseUser;
use AppBundle\User\Picture;
class UserPicture extends Picture implements ApplicationPicture
{
use FileHolderTrait;
/** @var string */
private $id;
/**
* UserPicture constructor.
* @param BaseUser $user
* @param File $file
*/
public function __construct(BaseUser $user, File $file)
{
$this->id = Uuid::uuid4();
$this->setFile($file);
parent::__construct($user);
}
}
Le trait FileHolderTrait apporte tous les attributs (file, fileName, fileSize, fileMimeType et fileOriginalName) et fonctions utiles pour le mapping avec le bundle VichUploaderBundle. Parmi ces attributs, seuls file et fileName sont obligatoires.
<?php
namespace AppBundle\Doctrine\Entity;
use Symfony\Component\HttpFoundation\File\File;
namespace AppBundle\Doctrine\Entity;
use Symfony\Component\HttpFoundation\File\File;
trait FileHolderTrait
{
/** @var File */
private $file;
/** @var string */
private $fileName;
/** @var integer */
private $fileSize;
/** @var string */
private $fileMimeType;
/** @var string */
private $fileOriginalName;
/**
* @return File
*/
public function getFile(): ?File
{
return $this->file;
}
/**
* @param null|File $file
*/
public function setFile(?File $file): void
{
$this->file = $file;
}
/**
* @return string
*/
public function getFileName(): ?string
{
return $this->fileName;
}
/**
* @param string $fileName
*/
public function setFileName(?string $fileName)
{
$this->fileName = $fileName;
}
/**
* @return int
*/
public function getFileSize(): ?int
{
return $this->fileSize;
}
/**
* @param int $fileSize
*/
public function setFileSize(?int $fileSize)
{
$this->fileSize = $fileSize;
}
/**
* @return string
*/
public function getFileMimeType(): ?string
{
return $this->fileMimeType;
}
/**
* @param string $fileMimeType
*/
public function setFileMimeType(?string $fileMimeType)
{
$this->fileMimeType = $fileMimeType;
}
/**
* @return string
*/
public function getFileOriginalName(): ?string
{
return $this->fileOriginalName;
}
/**
* @param string $fileOriginalName
*/
public function setFileOriginalName(?string $fileOriginalName)
{
$this->fileOriginalName = $fileOriginalName;
}
}
Un exemple de configuration Doctrine de l’entité UserPicture avec les attributs apportés par le FileHolderTrait et la relation avec une entité User.
AppBundle\Doctrine\Entity\UserPicture:
type: entity
table: user_picture
id:
id:
type: uuid
fields:
fileName:
type: string
fileSize:
type: integer
fileMimeType:
type: string
fileOriginalName:
type: string
oneToOne:
user:
targetEntity: AppBundle\User\BaseUser
mappedBy: avatar
Configuration des bundles VichUploader et LiipImagine
Les deux bundles ont besoin d’être configurés et possèdent beaucoup d’options paramétrables. Config.yml :
vich_uploader:
# Choix du driver de Base de donnée
db_driver: orm
metadata:
auto_detection: false
# Repertoire contenant la configuration du mapping, ici en Yaml mais peut être aussi défini via les annotations.
directories:
- {path: '%kernel.root_dir%/../src/AppBundle/Resources/config/uploader'}
mappings:
# nom du mapping qui sera utilisé ailleurs dans la configuration
user_image:
uri_prefix: /uploads/images/users
upload_destination: '%env(UPLOAD_DIR)%/images/users'
# spécification du namer utiliser pour le nom du fichier, il en existe plusieurs dans le bundle (https://github.com/dustin10/VichUploaderBundle/blob/master/Resources/doc/namers.md) mais il est aussi possible d’en créer
namer: vich_uploader.namer_hash
# spécification du namer utiliser pour les répertoires parents
directory_namer:
service: vich_uploader.directory_namer_subdir
options: {chars_per_dir: 3, dirs: 2}
delete_on_remove: true # suppression du fichier à la suppression de l’entité
delete_on_update: true # suppression du fichier quand un nouveau fichier est envoyé
liip_imagine:
# paramètre de chargement des fichiers d’origine
loaders:
# nom du loader
uploaded_files:
filesystem:
data_root:
- '%env(IMAGINE_FILESYSTEM_ROOT)%'
data_loader: uploaded_files
# Liste des filter set (ensemble de filtres) de l’application
filter_sets:
cache: ~
user_mini_thumbnail:
# il existe de nombreux filtres prédéfinis (http://symfony.com/doc/master/bundles/LiipImagineBundle/filters.html) mais il est aussi possible d’en créer.
quality: 75
filters:
auto_rotate: ~
thumbnail: { size: [52, 52], mode: outbound }
user_thumbnail:
quality: 75
filters:
auto_rotate: ~
thumbnail: { size: [100, 100], mode: outbound }
Ensuite, il y a aussi la configuration du mapping entre l’entité UserPicture et la configuration de vich_uploader.
AppBundle\Doctrine\Entity\UserPicture:
# nom de l’attribut qui contiendra le fichier
file:
mapping: user_image # nom du mapping de la configuration
filename_property: fileName # puis l’attribut qui contiendra le nom du fichier
size: fileSize
mime_type: fileMimeType
original_name: fileOriginalName
Logique d’accès aux images
Prenons un exemple de service qui utilise les deux nouveaux bundles pour récupérer une image.
Notre service BasePathPicture abstrait qui récupère le chemin du fichier ou un placeholder s'il n’existe pas, puis applique au fichier un filter set de LiipImagineBundle.
<?php
namespace AppBundle\Services\Picture;
use AppBundle\Doctrine\Entity\ApplicationPicture;
use Liip\ImagineBundle\Imagine\Cache\CacheManager;
use Vich\UploaderBundle\Templating\Helper\UploaderHelper;
abstract class BasePathPicture
{
/** @var UploaderHelper */
protected $uploaderHelper;
/** @var array */
protected $placeholders;
/** @var CacheManager */
protected $imagineCacheManager;
public function __construct(UploaderHelper $uploaderHelper, array $placeholders, CacheManager $imagineCacheManager)
{
$this->uploaderHelper = $uploaderHelper;
$this->placeholders = $placeholders;
$this->imagineCacheManager = $imagineCacheManager;
}
/**
* @param ApplicationPicture $picture
* @param string $filter
* @param string $fileField
* @param string $placeholderKey
* @return string
*/
protected function getPicturePath(?ApplicationPicture $picture, string $filter, string $fileField, string $placeholderKey): string
{
$picturePath = $picture ? $this->uploaderHelper->asset($picture, $fileField): $this->placeholders[$placeholderKey];
return $this->imagineCacheManager->getBrowserPath($picturePath, $filter);
}
}
Puis le service qui étend la classe abstraite précédente et appelle la fonction de récupération du lien de l’image demandée.
<?php
namespace AppBundle\Services\Picture;
use AppBundle\Doctrine\Entity\UserPicture;
class PathUserPicture extends BasePathPicture
{
/**
* @param UserPicture|null $userPicture
* @param string $filter
* @return string
*/
public function getPath(?UserPicture $userPicture, string $filter): string
{
return $this->getPicturePath($userPicture, $filter, 'file', 'avatar');
}
}
Un exemple d’appel du service PathUserPicture avec une twig extension.
<?php
namespace AppBundle\Twig;
use AppBundle\Doctrine\Entity\UserPicture;
use AppBundle\Services\Picture\PathUserPicture;
use Twig\Extension\AbstractExtension;
class PictureExtension extends AbstractExtension
{
/** @var PathUserPicture */
protected $pathUserPicture;
/**
* PictureExtension constructor.
* @param PathUserPicture $pathUserPicture
*/
public function __construct(
PathUserPicture $pathUserPicture
)
{
$this->pathUserPicture = $pathUserPicture;
}
public function getFilters()
{
return array(new \Twig_SimpleFilter('avatar', array($this, 'avatarFilter')));
}
public function avatarFilter(?UserPicture $userPicture, string $filter)
{
return $this->pathUserPicture->getPath($userPicture, $filter);
}
}
Ainsi lorsqu'on applique le filtre twig avatar sur un UserPicture, on récupère le chemin de l’image avec les bons filtres appliqués ou un placeholder si le fichier n’est pas trouvé.
{{ user.avatar|avatar('user_thumbnail') }}
Conclusion
Dans cet article, nous vous avons présenté une des solutions possibles avec Symfony et les bundles VichUploader et LiipImagine pour gérer facilement différents formats d’images. Ces deux bundles sont facilement personnalisables, n’hésitez pas à lire leurs documentations.
Documentation du VichUploaderBundle
Documentation du LiipImagineBundle