Manage multiple files upload with Symfony

July 20, 2015Julien Vallini4 min read

Note : this article has been written for Symfony 2. It is not working with Symfony 3.

The long and exhausting journey

Have you already told your product owner that the feature he was suggesting was too ambitious right now and that he should prioritize?
It is often the case when multiple files upload is on the table. Indeed, the symfony cookbook contains a very simple, yet detailed article describing how to setup a single file upload.

Multiple files upload is seen by most developers as a quite more complex task. The first thing that comes to mind is usually to:

  • Create a new class representing the file (containing at least its path)
  • Add a OneToMany relationship between the Document class and the File class
  • Add a form collection and write the necessary javascript to manage this collection (i.e. at least add/remove a file)
  • Adapt the Document methods to handle the form collection

The point of this article is to introduce a much faster way to set up multiple files upload through the usage of the fairly new multiple field option (implemented in Symfony 2.6).

The pragmatic and time-saving way

Let's start with the fileupload symfony cookbook.
To handle multiple files, the first thing to do is to adapt our form:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        ->add('name')
        ->add('files', 'file', array(
            'multiple' => true,
            'data_class' => null,
        ));
}

Try it and note that Symfony displays the HTML5 input multiple Attribute

When you submit the form, an array of UploadedFile is sent instead of a single object. Thus, we need to adapt our Document class to persist an array of paths instead of a single one:

/**
 * @ORM\Column(type="array")
 */
private $paths;

and it is necessary to adapt the upload() method to persist each file:

/**
 * @ORM\PreFlush()
 */
public function upload()
{
    foreach($this->files as $file)
    {
        $path = sha1(uniqid(mt_rand(), true)).'.'.$file->guessExtension();
        array_push ($this->paths, $path);
        $file->move($this->getUploadRootDir(), $path);

        unset($file);
    }
}

And that's all!

Hurray, we can now persist multiple files and it took us 5 minutes! Isn't it satisfying?

Now let's go further. What we have done is great, but lacks flexibility. Let's give a file its own entity. We will then be able to store some metadata such as its name or size.

The effortless elegant method

Okay, we now want our own File class. Let's create something simple. It will be easy to adapt it later if needed:

/**
 * @ORM\Table(name="files")
 * @ORM\Entity
 */
class File
{
    /**
     * @var integer
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /**
     * @var string
     *
     * @ORM\Column(name="path", type="string", length=255)
     */
    private $path;

    /**
     * @var string
     *
     * @ORM\Column(name="name", type="string", length=255)
     */
    private $name;

    /**
     * @var integer
     *
     * @ORM\Column(name="size", type="integer")
     */
    private $size;

    /**
     * @var UploadedFile
     */
    private $file;

    /**
     * @ORM\ManyToOne(targetEntity="Document", inversedBy="files")
     * @ORM\JoinColumn(name="document_id", referencedColumnName="id")
     **/
    private $document;

Then, we need to adapt our Document class:

/**
 * @var File
 *
 * @ORM\OneToMany(targetEntity="File", mappedBy="document", cascade={"persist"})
 *
 */
private $files;

/**
 * @var ArrayCollection
 */
private $uploadedFiles;

The attribute $uploadedFiles is necessary because this is the one which will be hydrated when the form is submitted.

Now let's adapt the upload method and instantiate our new class dynamically:

/**
 * @ORM\PreFlush()
 */
public function upload()
{
    foreach($this->uploadedFiles as $uploadedFile)
    {
        $file = new File();

        /*
         * These lines could be moved to the File Class constructor to factorize
         * the File initialization and thus allow other classes to own Files
         */
        $path = sha1(uniqid(mt_rand(), true)).'.'.$uploadedFile->guessExtension();
        $file->setPath($path);
        $file->setSize($uploadedFile->getClientSize());
        $file->setName($uploadedFile->getClientOriginalName());

        $uploadedFile->move($this->getUploadRootDir(), $path);

        $this->getFiles()->add($file);
        $file->setDocument($this);

        unset($uploadedFile);
    }
}

And we are done, awesome, we can now upload multiple files and populate in the same time entities to represent them!

Here is an implementation of what is described in this section.

The bundle polish

Okay. One might say that the HTML 5 multiple attribute is not (quite) the state of the art in terms of UI. Fair enough, let's introduce a magical bundle which will beautify your brand new multiple files upload feature.

Ladies and gentlemen, let me introduce OneupUploaderBundle. This bundle provides the choice between the most used file uploads javascript libraries.

The operation of this bundle is a little bit different from the first two sections of this article. In fact, submitting a file will trigger an ajax call on a specific url. The idea is then to create an event listener which will persist on the fly the incoming files.

For example, your edit page view will contain something like:

<div
  action="{{ oneup_uploader_endpoint('document_files') }}"
  id="portfolio"
  class="dropzone"
></div>

And your eventListener will contains a method catching the upload events:

/**
 * @param PostPersistEvent $event
 */
public function onUpload(PostPersistEvent $event)
{

Conclusion

Hopefully, this article gave you a clear overview of what is hidden behind multiple files uploads, as well as the keys to develop this feature quickly.

Don't hesitate to suggest any improvement, I will keep it up to date.


Sources

Julien Vallini

Julien Vallini

Web Developer at Theodo