JS File Upload with Dropzone and Symfony

Handling File Uploads completely server-side (especially with Symfony) doesn’t usually seem a pretty straightforward task. It requires a lot of tweaking for stuff as simple as displaying the upload progress or even generate thumbnails for images, etc. Besides, the documentation is pretty convoluted in itself. An alternate is to use the simplicity of JS File Upload with Dropzone and Symfony 3.x+ that’ll not only help fast-track development but also avoid one from rewriting the wheel.

Dropzone, IMHO, is one of the top libraries out there to handle file uploads that allows file previews(thumbnails), transferring files in chunks, viewing upload progress and a ton of other features. So how can we use this library with a Symfony app? Lets see how…

symfony dropzone

1. Include Dependencies:

Start by including the dependencies, dropzone.js and dropzone.css files in your twig template. Let’s say I have a base.html.twig template as follows

{% block stylesheets %}
    <link rel="stylesheet" href="{{ asset('assets/dropzone.min.css') }}">
{% endblock stylesheets %}
{% block javascripts %}
    <script src="{{ asset('assets/jquery.min.js') }}"></script>
    <script src="{{ asset('assets/dropzone.min.js') }}"></script>
{% endblock javascripts %}

2. Initialize/Configure Dropzone:

Dropzone attaches itself to any element having class ‘dropzone’, hence it is imperative that we set Dropzone.autoDiscover = false; which will disable dropzone’s default behaviour. We initialize dropzone with certain parameters (I won’t get much into its details, it being out of the scope of this tutorial) The most important configuration option is the url parameter. We set it to the path that will handle the uploaded file.

<!-- index.html.twig --> 
{% extends 'base.html.twig' %} <!-- extending ensures you loaded dropzone's assets -->
{% block body %}
     <h1 class="col-sm-offset-3">File Upload in Symfony using Dropzone</h1><hr>
     {#{{ form_start(form) }}#}
     {#{{ form_widget(form) }}#}
     <div class="form-group">
         <label class="col-sm-2 control-label required" for="">Default Image</label>
         <div class="col-sm-10">
             <div class="dropzone"></div>
         </div>
     </div>
     <div class="col-xs-4 pull-right">
         <input type="submit" class="btn btn-block btn-primary" value="Add" />
     </div>
     {#{{ form_end(form) }}#}
{% endblock body %}
{% block javascripts %}
    {{ parent() }}
    
<script>
        // init,configure dropzone
        Dropzone.autoDiscover = false;
        var dropzone_default = new Dropzone(".dropzone", {
            url: '{{ path('fileuploadhandler') }}' ,
            maxFiles: 1,
            dictMaxFilesExceeded: 'Only 1 Image can be uploaded',
            acceptedFiles: 'image/*',
            maxFilesize: 3,  // in Mb
            addRemoveLinks: true,
            init: function () {
                this.on("maxfilesexceeded", function(file) {
                    this.removeFile(file);
                });
                this.on("sending", function(file, xhr, formData) {
                    // send additional data with the file as POST data if needed.
                    // formData.append("key", "value");  
                });
                this.on("success", function(file, response) {
                    if (response.uploaded) 
                        alert('File Uploaded: ' + response.fileName);
                });
            }
        });
    </script>
{% endblock javascripts %}

3. Handle Uploaded File in the Controller:

As seen above, we have used url: '{{ path('fileuploadhandler') }}' as the path. So lets write the logic to handle the file that will be sent to this URL. (You may wanna check out the Official Docs for an in-depth file-upload handling logic)

 // AppBundle/Controller/DefaultController.php
 // use SymfonyComponentHttpFoundationJsonResponse;

 // We define the route using annotations
 /**
  * @Route("/fileuploadhandler", name="fileuploadhandler")
  */
 public function fileUploadHandler(Request $request) {
     $output = array('uploaded' => false);
     // get the file from the request object
     $file = $request->files->get('file');
     // generate a new filename (safer, better approach)
     // To use original filename, $fileName = $this->file->getClientOriginalName();
     $fileName = md5(uniqid()).'.'.$file->guessExtension();
     // Note: While using $file->guessExtension(), sometimes the MIME-guesser may fail silently for improperly encoded files. It is recommended to use a fallback for such cases if you know what file extensions are expected. (You can loop-over the allowed file extensions or even hard-code it if you expect only a particular type of file extension.)

     // set your uploads directory
     $uploadDir = $this->get('kernel')->getRootDir() . '/../web/uploads/';
     if (!file_exists($uploadDir) && !is_dir($uploadDir)) {
         mkdir($uploadDir, 0775, true);
     }
     if ($file->move($uploadDir, $fileName)) { 
        $output['uploaded'] = true;
        $output['fileName'] = $fileName;
     }
     return new JsonResponse($output);
 }
// You may wrap the above code in a "try/catch" block to handle unforeseen exceptions.

Running the code now should give you a screen as below. Uploading a file should work at this point. (If it does not, it is probably due to missing permissions. Ensure that the uploads directory is writable by your webserver)

JSfileUploadWithSymfonyAndDropzone

4. Persist to Database:

Now, even though the file gets uploaded, we do not have a reference to it in order to display the file to the enduser. To change that, lets define an entity to store related information. For this basic tutorial, we store only the filename, but in a real use-case, you could store the directory information, or any other meta-data as per your needs.

// AppBundle/Entity/mediaEntity.php
// use DoctrineORMMapping as ORM;
/**
* @ORMId
* @ORMGeneratedValue(strategy="AUTO")
* @ORMColumn(type="integer")
*/
private $id;

/**
* @ORMColumn(length=100, unique=true, nullable=false)
*/
private $fileName;

// getters and setters ...

Synchronize your database with this newly created mediaEntity by executing
php bin/console doctrine:schema:update --force in the console. If you wish to see the RAW SQL queries before synchronizing, you can use php bin/console doctrine:schema:update --dump-sql. Once the database is synchronized, it is time to populate it with real data. For this, we will need to update the controller as follows:

 // AppBundle/Controller/DefaultController.php
 // use SymfonyComponentHttpFoundationJsonResponse;

 /**
  * @Route("/fileuploadhandler", name="fileuploadhandler")
  */
 public function fileUploadHandler(Request $request) {
     $output = array('uploaded' => false);
     // get the file from the request object
     $file = $request->files->get('file');
     // generate a new filename 
     $fileName = md5(uniqid()).'.'.$file->guessExtension();

     // set your uploads directory
     $uploadDir = $this->get('kernel')->getRootDir() . '/../web/uploads/';
     if (!file_exists($uploadDir) && !is_dir($uploadDir)) {
         mkdir($uploadDir, 0775, true);
     }
     if ($file->move($uploadDir, $fileName)) {
         // get entity manager
         $em = $this->getDoctrine()->getManager();

         // create and set this mediaEntity
         $mediaEntity = new mediaEntity();
         $mediaEntity->setFileName($fileName);

         // save the uploaded filename to database
         $em->persist($mediaEntity);
         $em->flush();
         $output['uploaded'] = true;
         $output['fileName'] = $fileName;
     };

     return new JsonResponse($output);
 }

Summary

You could now try to upload a file and check the media table in your database. It should have a new record inserted consisting of the uploaded filename. Similarly, it is also possible to delete the file from the server and its linked record from the database. Head over to the complete Source Code which portrays this delete functionality as well.
Drop comments in case of any queries. Hope this helps! 🙂