Manage multiple files upload with Symfony
Julien Vallini5 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
- Stack Overflow Article which gaves me the original idea
- Symfony cookbook page about file upload which helped to start this tutorial from a good and well-documented example