Accueil /  Blog / Symfony / Librarie Open Source : PrestaImageBundle

Librarie Open Source : PrestaImageBundle

Publié le mercredi 3 mars 2021

Le bundle open source made by Prestaconcept qui permet à vos utilisateurs de redimensionner une image à l'upload.

Besoin

Charger une image sur son site est un besoin fréquent, par exemple pour enrichir son compte d'une photo de profil, illustrer un article de blog ou un produit de sa boutique en ligne.

Problématique

Pour respecter la mise en page du site, il est généralement nécessaire de contraindre les images dans un format et/ou des dimensions bien précises, ce qui conduit souvent à des images aplaties ou étirées. Or un utilisateur lambda a rarement cette notion à l'esprit lorsqu'il choisit son image, et encore plus rarement les outils adaptés pour la redimensionner.

Solution

C'est là qu'intervient ce bundle.

En couplant la librairie JavaScript Cropper.js avec Symfony, il permet à vos utilisateurs de redimensionner une image en ligne en amont de son chargement sur le serveur, selon un format adapté pour son usage que vous aurez potentiellement contraint au préalable.

Cela permet à vos utilisateurs de garder le contrôle sur l'image qu'ils souhaitent charger tout en vous évitant d'avoir à la redimensionner côté serveur avec le risque de la déformer ou de perdre le sujet de l'image.

Installation

Ajoutez le bundle en dépendance de votre projet.

composer require presta/image-bundle

Si vous n'êtes pas encore passés sous symfony/flex (vous devriez y songer), ajoutez les bundles suivants à votre kernel.

new Vich\UploaderBundle\VichUploaderBundle(),
new Presta\ImageBundle\PrestaImageBundle(),

Importez les routes du bundle.

presta_image:
    resource: "@PrestaImageBundle/Resources/config/routing.yml"

Configurez Twig pour utiliser le thème du formulaire (ou développez le vôtre).

twig:
    form_themes:
        - "@PrestaImage/form/bootstrap_4.html.twig"

Configurez VichUploader.

vich_uploader:
    db_driver: 'orm'

    mappings:
        profile_image:
            uri_prefix: '/images/profiles'
            upload_destination: '%kernel.project_dir%/public/images/profiles'
            namer: 'Vich\UploaderBundle\Naming\SmartUniqueNamer'

Installez les dépendances JavaScript.

npm install jquery-cropper jquery cropperjs bootstrap

Incluez les scripts et les styles suivants sur votre page.

/path/to/jquery.min.js
/path/to/cropper.min.js
/path/to/cropper.min.css
/path/to/jquery-cropper.min.js
/path/to/bootstrap.min.js
/path/to/bootstrap.min/css
@PrestaImageBundle/Resources/public/css/cropper.css
@PrestaImageBundle/Resources/public/js/cropper.js

Initialisez la librairie Cropper du bundle.

(function(w, $) {

    'use strict';

    $(function() {
        $('.cropper').each(function() {
            new Cropper($(this));
        });
    });

})(window, jQuery);

Alternative avec Webpack Encore.

Installation

npm install --save-dev @symfony/webpack-encore node-sass sass-loader webpack-notifier

Fichier de configuration (webpack.config.js)

const Encore = require('@symfony/webpack-encore');
const path = require('path');

Encore
    .setOutputPath('public/build/')
    .setPublicPath('/build')

    .addEntry('js/app', './assets/js/app.js')
    .addStyleEntry('css/app', './assets/css/app.scss')

    .cleanupOutputBeforeBuild()
    .enableBuildNotifications()
    .enableSourceMaps(!Encore.isProduction())
    .enableVersioning(Encore.isProduction())

    .addAliases({
        prestaimage: path.resolve(__dirname, 'public/bundles/prestaimage')
    })

    .enableSassLoader()
    .autoProvidejQuery()
;

module.exports = Encore.getWebpackConfig();

Fichier de styles (assets/css/app.scss)

@import "~bootstrap/scss/bootstrap";
@import "~cropper/dist/cropper.min.css";
@import "../../public/bundles/prestaimage/css/cropper.css";

Fichier de scripts (assets/js/app.js)

import 'bootstrap';
import 'cropper/dist/cropper.min'
import * as Cropper from 'prestaimage/js/cropper';

$(function() {
    $('.cropper').each(function() {
        new Cropper($(this));
    });
});

Voilà, c'est installé. Ne reste plus qu'à développer vos formulaires d'upload d'images et à les intégrer.

Cas d'usage

Imaginons que vous souhaitiez permettre à vos utilisateurs de charger une image de profil au format 4:3.

Création de l'entité.

<?php

namespace App\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Entity\File as EmbeddedFile;
use Vich\UploaderBundle\Mapping\Annotation as Vich;

/**
 * @ORM\Entity
 * @Vich\Uploadable
 */
class User
{
    /**
     * @var int|null
     * 
     * @ORM\Id()
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * NOTE: This is not a mapped field of entity metadata, just a simple property.
     * 
     * @var File|null
     * 
     * @Vich\UploadableField(mapping="profile_image", fileNameProperty="picture.name", size="picture.size", mimeType="picture.mimeType", originalName="picture.originalName", dimensions="picture.dimensions")
     */
    private $pictureFile;

    /**
     * @var EmbeddedFile
     *
     * @ORM\Embedded(class="Vich\UploaderBundle\Entity\File")
     */
    private $picture;

    /**
     * @var \DateTimeImmutable|null
     *
     * @ORM\Column(type="datetime_immutable")
     */
    private $updatedAt;

    public function __construct()
    {
        $this->picture = new EmbeddedFile();
    }

    public function setPictureFile(?File $pictureFile = null): void
    {
        $this->pictureFile = $pictureFile;

        if (null !== $pictureFile) {
            // It is required that at least one field changes if you are using doctrine
            // otherwise the event listeners won't be called and the file is lost
            $this->updatedAt = new \DateTimeImmutable();
        }
    }

    public function getPictureFile(): ?File
    {
        return $this->pictureFile;
    }

    public function setPicture(EmbeddedFile $picture): void
    {
        $this->picture = $picture;
    }

    public function getPicture(): ?EmbeddedFile
    {
        return $this->picture;
    }
}

Création du contrôleur.

<?php

namespace App\Controller;

use App\Entity\User;
use App\Form\Type\ProfileType;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

class ProfileController extends AbstractController
{
    public function __invoke(Request $request, EntityManagerInterface $entityManager, User $user): Response
    {
        $form = $this->createForm(ProfileType::class, $user);
        $form->handleRequest($request);

        if (!$form->isSubmitted() || !$form->isValid()) {
            return $this->render('profile.html.twig', [
                'form' => $form->createView(),
            ];
        }

        $entityManager->flush();

        return $this->redirectToRoute('...');
    }
}

Création du template Twig.

{% extends 'base.html.twig' %}

{% block body %}
    {{ form(form) }}
{% endblock %}

Création du formulaire d'upload d'image.

<?php

namespace App\Form\Type;

use App\Entity\User;
use Presta\ImageBundle\Form\Type\ImageType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ProfileType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('picture', ImageType::class)
        ;
    }

    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver
            ->setDefault('data_class', User::class)
        ;
    }
}

Par défaut, la liste des formats disponibles est la suivante : 16:9, 4:3, 1, 2:3, Libre, mais vous pouvez la configurer à votre guise.

use Presta\ImageBundle\Model\AspectRatio;

$builder
    ->add('picture', ImageType::class, [
        'aspect_ratios' => [
            '3_4' => new AspectRatio(3/4, 'translation_path.3_4', true),
            '3_2' => new AspectRatio(1.5, 'translation_path.3_2', false),
        ],
    ])
;

Pour voir la liste des options disponibles et plus de détails sur l'intégration du bundle, vous pouvez vous reporter au Dépôt GitHub du projet.

Conclusion

Grâce à ce bundle, finis les problèmes d'images aplaties ou étirées. Vous pouvez contraindre le format d'image en préalable à son chargement, tout en fournissant un moyen simple et intégré à vos utilisateurs pour qu'ils gardent le contrôle sur les images qu'ils vont charger.

Next steps

La sortie récente de Symfony UX pourrait permettre une meilleure intégration de ce bundle à l'écosystème Symfony grâce notamment au composant Symfony UX Cropper.js. Utiliser ce composant pourrait notamment permettre d'automatiser l'installation des dépendances JavaScript, afin de simplifier le processus d'installation.

Cette refonte pourrait également être l'occasion de se séparer de jQuery en réécrivant le code en JavaScript natif. On pourrait aussi en profiter pour proposer un template standard indépendant de Bootstrap.

Voici plusieurs axes d'évolution à considérer dans les semaines ou les mois à venir...

Suivez notre actualité en avant première. Pas plus d’une newsletter par mois.