Image de couverture de l'article Tuto : Symfony3 et DropzoneJS

DropzoneJS est une bibliothèque open-source permettant le drag'n'drop de fichiers dans une zone tout en affichant une miniature à l'envoi ainsi qu'une barre de progression. Voyons ensemble comment le câbléer à une architecture Symfony3.

^ Ce tuto a été réalisé avec une version 3.0.* de Symfony. Peut être que dans le futur, la récupération des lignes de code ci-dessous ne marchera plus ;) !

DropzoneJS

DropzoneJS est une biblithèque javascript, rapide à mettre en place, qui combine toutes les fonctionnalités d'un uploader. Très pratique et relativement clé en main, elle fait gagner beaucoup de temps ! Nous avons très rapidement un rendu idéal.

Parmi toute la panoplie des uploaders ou des file managers que nous pouvons trouver en ligne, DropzoneJS est une des bibliothèques que je trouve la plus pratique à utiliser... Voici à quoi s'attendre rapidement :

Aperçu de dropzoneJS

A savoir que toute la doc de DropzoneJS se trouve à cette adresse.

Téléchargements des sources de DropzoneJS

Nous allons faire ça à l'ancienne. Débutez par télécharger les sources :

Je vous invite à les placer dans votre dossier contenant vos ressources publiques, puis à charger vos scripts avec votre méthode préférée. Cependant, en gros, ça devrait jamais vraiment s'éloigner de...

 <script src="./path/to/dropzone.js"></script>

@ À noter que DropzoneJS peut aussi être installée via Bower, "dropzone": "4.3.*"

Câblage de DropzoneJS avec Symfony3

Tout d'abord, vous devez définir dans votre template (par exemple uploader.html.twig) votre zone qui accueillera vos glisser/déposer ou votre invitation à l'upload. Il s'agit dans le cas de DropzoneJS, par défaut, d'une div de classe dropzone.

<div class="dropzone my-dropzone" id="form_snippet_image" action="{{path('ajax_snippet_image_send')}}">
</div>

Ensuite, dans votre Javascript, définissez cette zone comment étant celle de votre dropzone.

//je récupère l'action où sera traité l'upload en PHP
var _actionToDropZone = $("#form_snippet_image").attr('action');

//je définis ma zone de drop grâce à l'ID de ma div citée plus haut.
Dropzone.autoDiscover = false;
var myDropzone = new Dropzone("#form_snippet_image", { url: _actionToDropZone });

A ce stade, votre div devrait s'être transformée en zone d'accueil de drop de fichier ! Vos fichiers glissés ne sont pas encore uploadés sur votre serveur mais nous pouvons déjà voir que DropzoneJS fait son travail. Par exemple, dans votre Javascript, insérez le code suivant :

myDropzone.on("addedfile", function(file) {
            alert('nouveau fichier reçu');
        });

La petite fonction précédente devrait vous faire afficher un message à chaque fois qu'un fichier est ajouté à votre zone de drop. J'ai ici utilisé l'événement addedfile, mais toute une liste bien pratique est disponible à cette adresse

Uploader les fichiers

Pour que vos images ou documents soient correctement déplacés où vous le souhaitez lorsqu'ils sont déposés dans notre Dropzone, je me suis inspiré de la documentation officielle de Symfony3 sur la gestion des uploads.

Rappelez-vous, dans notre code ci-dessus, j'ai indiqué :

action="{{path('ajax_snippet_image_send')}}"

C'est dans l'action associée à la route ajax_snippet_image_send que nous traiterons nos fichiers/images. A chaque ajout d'un document dans la zone de drop, DropzoneJS enverra ce fichier sur la route définie juste au dessus.

Création de l'entité des documents

Si vous n'avez pas d'entité liée à vos médias/documents, vous êtes invités à le faire. C'est très pratique pour manager tout ça par la suite (ré-utilisation de fichiers anciennements uploadé, par exemple !).

Nous créons donc une entité Document.php ( toujours fortement inspiré de la doc officielle )

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\Table(name="document")
 * @ORM\HasLifecycleCallbacks
 * @ORM\Entity(repositoryClass="AppBundle\Repository\DocumentRepository")
 */
class Document
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     */
    public $name;

    /**
     * @Assert\File(maxSize="6000000")
     */
    private $file;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    public $path;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
         $this->file = $file;
        // check if we have an old image path
        if (isset($this->path)) {
            // store the old name to delete after the update
            $this->temp = $this->path;
            $this->path = null;
        } else {
            $this->path = 'initial';
        }
    }

    /**
     * Get file.
     *
     * @return UploadedFile
     */
    public function getFile()
    {
        return $this->file;
    }

    public function getAbsolutePath()
    {
        return null === $this->path
            ? null
            : $this->getUploadRootDir().'/'.$this->path;
    }

    public function getWebPath()
    {
        return null === $this->path
            ? null
            : $this->getUploadDir().'/'.$this->path;
    }

    protected function getUploadRootDir()
    {
        // the absolute directory path where uploaded
        // documents should be saved
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    protected function getUploadDir()
    {
        // get rid of the __DIR__ so it doesn't screw up
        // when displaying uploaded doc/image in the view.
        return 'uploads/documents';
    }

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        // la propriété « file » peut être vide si le champ n'est pas requis
        if (null === $this->file) {
            return;
        }

        if ($this->path != $this->file->getClientOriginalName()) {
            $this->path = $this->file->getClientOriginalName();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        // la propriété « file » peut être vide si le champ n'est pas requis
        if (null === $this->file) {
            return;
        }

        $file_name = $this->file->getClientOriginalName();

        // la méthode « move » prend comme arguments le répertoire cible et
        // le nom de fichier cible où le fichier doit être déplacé
        if (!file_exists($this->getUploadRootDir())) {
            mkdir($this->getUploadRootDir(), 0775, true);
        }
        $this->file->move(
            $this->getUploadRootDir(), $file_name
        );
        $this->file = null;
    }

    /**
     * Get id
     *
     * @return integer
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set name
     *
     * @param string $name
     *
     * @return Document
     */
    public function setName($name)
    {
        $this->name = $name;

        return $this;
    }

    /**
     * Get name
     *
     * @return string
     */
    public function getName()
    {
        return $this->name;
    }

    /**
     * Set path
     *
     * @param string $path
     *
     * @return Document
     */
    public function setPath($path)
    {
        $this->path = $path;

        return $this;
    }

    /**
     * Get path
     *
     * @return string
     */
    public function getPath()
    {
        return $this->path;
    }
}

Votre entité est créée, un petit php bin/console doctrine:schema:update --force pour synchroniser votre base de données, et on est bon pour la suite !

@ Le dossier dans lequel seront transférées mes images est défini dans la méthode getUploadDir de l'entité Document.php. C'est à dire : 'uploads/documents'. Vous n'êtes pas obligés de créer ce dossier au préalable, un test d'existence est présent dans la méthode upload() de l'entité Document.php.

Création d'un controller de gestion du document envoyé par DropzoneJS

Voici l'action qui, appelée par DropzoneJS, traitera l'envoi de document.

    /**
     *
     * @Method({"GET", "POST"})
     * @Route("/ajax/snippet/image/send", name="ajax_snippet_image_send")
     */
    public function ajaxSnippetImageSendAction(Request $request)
    {
        $em = $this->container->get("doctrine.orm.default_entity_manager");

        $document = new Document();
        $media = $request->files->get('file');

        $document->setFile($media);
        $document->setPath($media->getPathName());
        $document->setName($media->getClientOriginalName());
        $document->upload();
        $em->persist($document);
        $em->flush();

        //infos sur le document envoyé
        //var_dump($request->files->get('file'));die;
        return new JsonResponse(array('success' => true));
    }

et voilà !

Et voilà, votre uploader est terminé ! Il est sommaire, mais il fonctionne ! Vous êtes invités à sécuriser ou à améliorer toute cette interface grâce à toute la panoplie d'événements et de méthodes que possède DropzoneJS !

N'hésitez pas à me faire vos retours d'utilisations, d'expériences (ou même à me remonter des erreurs s'il y en a ;) !)

Enfin, vous êtes invités à nous interpeller sur notre Twitter @agence_wanadev ou en commentaire ci-dessous !

Commentaires

Photo de HockFird auteur du commentaire

HockFird

Il y a 7 ans

Bonjour, 
Après avoir recopié le Cablage pour Dropzone. Ma console m'affiche "Uncaught Error : Dropzone already attached" et donc mon alert ne s'active pas. 

Pourtant mon seul code dropzone présent dans mon JS est le votre..
Mon DOM est bien chargé et mon JS se charge après le JQUERY et le Dropzone.js.

Auriez-vous une solution ? 
Merci 

A priori, cette erreur survient quand la drop zone est chargée 2 fois. Avez-vous bien indiqué Dropzone.autoDiscover = false; ?

Photo de wisdom auteur du commentaire

wisdom

Il y a 7 ans

Bonjour je voudrais savoir comment ça se passe au niveau de la modification, c'est à dire est ce que la modification d'une entité avec une image (exple: profil avec photo) écrasera l'image en base si jamais l'on a modifié que le nom sur notre profil ?

Bonjour Wisdom,

Dans l'exemple de l'article, oui, l'image sera bêtement et vulgairement modifiée. Le traitement du fichier est vraiment très primaire ici : on prend le fichier, on l'upload dans le dossier des uploads. Point. Plus violent, si une image uploadée possède le même nom qu'une image déjà présente dans le dossier, elle est écrasée ! Mais ça, évidemment, c'est avec le bête bout de code de l'article. Il faut bien sûr améliorer tout le process de gestion du fichier selon les besoins.

Photo de Marchive auteur du commentaire

Marchive

Il y a 8 ans

Bonjour je me pose la question comment faire quand on à un formulaire imbriqué ? 

Bonjour Marchive, merci pour ton commentaire. Alors, dans le cas de DropzoneJS je dois me pencher dessus plus sérieusement pour vous faire un exemple, mais vous pouvez déjà regarder l'article de mon collègue Jocelyn : https://www.wanadev.fr/26-symfony2-et-l-affichage-des-formulaires-imbriques/

A bientôt 

Photo de Tom auteur du commentaire

Tom

Il y a 7 ans

Merci :) !

Photo de Jean-Yves auteur du commentaire

Jean-Yves

Il y a 7 ans

Bonjour, merci pour ce tuto qui pose bien les bases d'un upload avec dropzone. Par contre, un truc que je n'ai pas pigé pour que l'upload fonctionne est: où instancie t'on la méthode upload dans l'entité Document ? Pour l'instant je sèche, merci d'avance :)

Jean-Yves, vous avez parfaitement raison. J'étais sûr d'avoir mis une démonstration complète, et pourtant... Il y a (bien évidemment) une fonction upload à appeler ! 

$document->upload();

directement dans la méthode ajaxSnippetImageSendAction utilisée en exemple. Je corrige ça dans l'article !

Merci, et bonne continuation.


Photo de Johann auteur du commentaire

Johann

Il y a 7 ans

        Bonjour et merci pour ce super tuto !

Concernant la prise en compte de l'action je rencontre un soucis. En effet lors de l'exécution de la page seule l'action spécifiée dans les paramètres de DropZone.js est prise en compte au lieu de celle spécifiée dans la div class = "dropzone" ( à savoir path('ajax_snippet_image_send')). Comment faites vous pour contourner ce problème ?

Bonjour Johann, je vous remercie pour commentaire.

Qu'entendez-vous par "l’exécution de la page" ? Le chargement de la page où se trouve votre zone de drop ? La route indiquée dans la la div de classe Dropzone devrait n'être utilisée qu'avec un évenement par la suite. (Mais je comprends peut être mal votre question).

N'hésitez pas à revenir ici ;-) !

Photo de Johann auteur du commentaire

Johann

Il y a 7 ans

Le chargement des libs Dropzone ne semble pas être le problème.

Mon script est rédigé tel que suit :

window.onload = function () {
var _actionToDropZone = $("#form_snippet_file").attr('action');

Dropzone.autoDiscover = false;
var fileDropZoneArea = new Dropzone("#form_snippet_file", {url: _actionToDropZone});

fileDropZoneArea.on("addedfile", function (file) {
alert("fichier reçu !");
});
};

Et ce script est déclaré dans un twig dont héritent tous les autres.


Photo de Johann auteur du commentaire

Johann

Il y a 7 ans

En plus Dropzone charge bien parque la zone apparaît bien sur ma page dc je ne vois vraiment pas d'où ça viens


Photo de Johann auteur du commentaire

Johann

Il y a 7 ans

Pour faire court, lorsque je drop le fichier dans la zone de drop la requête Ajax n’aboutit pas.

Photo de wisdom auteur du commentaire

wisdom

Il y a 7 ans

je voudrais savoir comment faire parce que je suis me trouve dans un cas similaire actuellement sans pouvoir trouver de solution, l'idée est que j'offre la possibilité d'ajouter plusieurs images à un article, mais en cas de modification de l'article si l'utilisateur de reUpload pas les images et qu'il soumet le formulaire, les images préenregistré sont bêtement écraser par un contenue vide, ce qui n'est pas très intéressant. Et je ne sais pas comment faire pour me sortir de là !

Merci !!!

Implémenté en quelques minutes :)

Photo de Sanchez auteur du commentaire

Sanchez

Il y a 7 ans

Bonjour, 

Merci beaucoup pour ce tuto très bien fait. Je cherche à mettre en place la suppression du fichier également.

Pouvez-vous m'aidez svp.

J'ai réussi à afficher le lien de suppression mais je n'arrive pas à déclencher une action dans mon contrôler depuis ce lien.

Visuellement le fichier disparaît de la dropzone mais je voudrais éxécuter une action dans mon contrôleur pour supprimer le fichier côté serveur.

D'avance merci de votre aide.



 

Photo de Clem69 auteur du commentaire

Clem69

Il y a 7 ans

Bonjour,
Des idées pour de l'upload de dossiers en SYMFONY3? En full PhP (Mais en cherchant je suis tombé ici) j'ai besoins de recréer la même architecture sur mon serveur :/

Merchiii !


Photo de Ali auteur du commentaire

Ali

Il y a 7 ans

Bonjour, 

Merci pour ce petit tuto.

j'ai eu un petit problème en essayant de l'utiliser sur mon site. 

La dropzone marche bien, cependant, les fichiers ne sont pas  enregistrés ( ni en bdd ni dans le dossier de destination ) .


Qaund j’inspecte avec la console la page je tombre sur cette erreur : 

"POST http://monsite.dev/ajax/snippet/image/send 500 (Internal Server Error) "

quand je clique dessus pour voir de quelle ligne il s'agit, ca me dis que l'erreur est dans le return de cette fonction : 

   Dropzone.prototype.submitRequest = function(xhr, formData, files) {

      return xhr.send(formData);

    };


auriez-vous une idée sur quoi il peut s'agir svp ?

Photo de Johann auteur du commentaire

Johann

Il y a 7 ans

Lorsque je charge ma page j'ai une erreur "ReferenceError: Dropzone is not defined" qui apparaît.

Essayez de chargez votre code relatif à Dropzone une fois que tout est bien chargé :

window.onload = function() {
//ici 
};

Dropzone est un script externe, il faut bien entendu qu'il soit chargé avant

(de chargeR, bien sûr... aîe...)

Photo de haithem auteur du commentaire

haithem

Il y a 7 ans

la fonction (ajaxSnippetImageSendAction) ou on la met exactement ? on fait un controller séparé ??