Optimisation des images : focus sur VichUploader et LiipImagine

Fabien

Fabien, Architecte web 27 avril 2018

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