Upload Multiple Images In Symfony2 With Validation On A Single Entity Property
I wanted a way to upload files/images that would all be tied to just a single property on the entity object. The @var file
field type declared for properties in the entity can only validate a single uploaded file, as the Symfony\Component\HttpFoundation\File\UploadedFile
class expects a string. I wanted to handle multiple files uploaded, (array of files).
Like everything, there is more than one way to do something, and below is the solution I implemented. This tutorial doesn’t cover how to make the multiple file uploader pretty, this just covers backend functionality.
The Entity File
Let’s first take a look at a very simplified entity, only showing the property “images” which I wanted to store in the database as an array of image paths to my web/media folder. I made my property nullable so it can be optional on the form.
namespace YourFolder\YourBundle\Entity; use Doctrine\ORM\Mapping as ORM; /** * EntityName * @ORM\Table(name="entityname") */ class Review { /** * @var array * * @ORM\Column(name="images", type="array", nullable=true) */ private $images; }
The Form Type File
Now let’s take a look at the form type which actually builds the form for the output. We’re adding two attributes to the images input; accept images only, and multiple. Again, this is a very simplified example, all your other properties would need to be added to the builder as well.
namespace YourFolder\YourBundle\Form; use Symfony\Component\Form\AbstractType; // more use statements ... class FormType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('images', 'file', array( 'attr' => array( 'accept' => 'image/*', 'multiple' => 'multiple' ) )) ; } // other functions here ... }
The Controller
This controller action isn’t exactly skinny, some of the checks could be moved out into other private functions, however for the ease of this blog post, let’s proceed to party. Basically, below we check if the form POST has any files set (images property). If it does, then I fire off the validation and uploading to a service. We’ll look at that next.
// ... other actions and annotations public function createAction(Request $request) { $entity = new Review(); $form = $this->createCreateForm($entity); $form->handleRequest($request); if ($form->isValid()) { // Handle the uploaded images $files = $form->getData()->getImages(); } // If there are images uploaded if($files[0] != '') { $constraints = array('maxSize'=>'1M', 'mimeTypes' => array('image/*')); $uploadFiles = $this->get('your_namespace.fileuploader')->create($files, $constraints); } if($uploadFiles->upload()) { $entity->setImages($uploadFiles->getFilePaths()); } else { // If there are file constraint validation issues foreach($uploadFiles->getErrors() as $error) { $this->get('session')->getFlashBag()->add('error', $error); } return array( 'entity' => $entity, 'form' => $form->createView(), ); } // ... persist, flush, success message, redirect, other functionality }
Services Configuration
I like using YAML, and basically always choose to use it over XML when possible. The services.yml file is pretty straight forward. Set up your service with your namespace and then inject the entity manager, the request stack, the validator, and the kernel. You’ll see why in the next section!
services: your_namespace.fileuploader: class: Namespace\YourBundle\Services\FileUploader arguments: [ @doctrine.orm.entity_manager, @request_stack, @validator, @kernel ] // ... other services
Creating The Service (FileUploader) Class
Finally, let’s look at the actual FileUploader class which I set up as a service. Now we can call this service from anywhere in our app, keeping the code DRY. You can see in the construct where I injected all the services I needed from the services.yml file above.
Summing up this file, it creates an object with the files and their constraints, loops through each file to test them against the constraints and returns true (uploads and moves file) or returns false (prints out the list of constraint violations).
namespace YourFolder\YourBundle\Services; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\File\Exception\FileException; use Symfony\Component\Validator\Constraints\File; use Doctrine\ORM\EntityManager; use Symfony\Component\Validator\ValidatorInterface; use Symfony\Component\HttpKernel\Kernel; class FileUploader { // Entity Manager private $em; // The request private $request; // Validator Service private $validator; // Kernel private $kernel; // The files from the upload private $files; // Directory for the uploads private $directory; // File pathes array private $paths; // Constraint array private $constraints; // Array of file constraint object private $fileConstraints; // Error array private $errors; public function __construct(EntityManager $em, RequestStack $requestStack, Validator $validator, Kernel $kernel) { $this->em = $em; $this->request = $requestStack->getCurrentRequest(); $this->validator = $validator; $this->kernel = $kernel; $this->directory = 'web/uploads'; $this->paths = array(); $this->errors = array(); } // Create FileUploader object with constraints public function create($files, $constraints = NULL) { $this->files = $files; $this->constraints = $constraints; if($this->constraints) { $this->fileConstraints = $this->createFileConstraint($this->constraints); } return $this; } // Upload the file / handle errors // Returns boolean public function upload() { if(!$this->files) { return true; } foreach($this->files as $file) { if(isset($file)) { if($this->fileConstraints) { $this->errors[] = $this->validator->validateValue($file, $this->fileConstraints); } $extension = $file->guessExtension(); if(!$extension) { $extension = 'bin'; } $fileName = $this->createName().'.'.$extension; $this->paths[] = $fileName; if(!$this->hasErrors()) { $file->move($this->getUploadRootDir(), $fileName); } else { foreach($this->paths as $path) { $fullpath = $this->kernel->getRootDir() . '/../' . $path; if(file_exists($fullpath)) { unlink($fullpath); } } $this->paths = null; return false; } } } return true; } // Get array of relative file paths public function getFilePaths() { return $this->paths; } // Get array of error messages public function getErrors() { $errors = array(); foreach($this->errors as $errorListItem) { foreach($errorListItem as $error) { $errors[] = $error->getMessage(); } } return $errors; } // Get full file path private function getUploadRootDir() { return $this->kernel->getRootDir() . '/../'. $this->directory; } // Generate random string for file name private function createName() { // Entity manager $em = $this->em; // Get Form request $form_data = $this->request->request->get('kmv_ampbundle_review'); // Get brand name $brand_name = $em->getRepository('KmvAmpBundle:Brand')->find($form_data['brand'])->getName(); $brand_name = str_replace(' ', '-', $brand_name); // Get model name $model_name = $em->getRepository('KmvAmpBundle:Model')->find($form_data['model'])->getName(); $model_name = str_replace(' ', '-', $model_name); // Create name $image_name = strtolower($brand_name.'-'.$model_name).'-'.mt_rand(0,9999); return $image_name; } // Create array of file constraint objects private function createFileConstraint($constraints) { $fileConstraints = array(); foreach($constraints as $constraintKey => $constraint) { $fileConstraint = new File(); $fileConstraint->$constraintKey = $constraint; if($constraintKey == "mimeTypes") { $fileConstraint->mimeTypesMessage = "The file type you tried to upload is invalid."; } $fileConstraints[] = $fileConstraint; } return $fileConstraints; } // Check if there are constraint violations private function hasErrors() { if(count($this->errors) > 0) { foreach($this->errors as $error) { if($error->__toString()) { return true; } } } return false; } }
The Twig View File
Below is how I output my html with the entity properties. The trick here is to concatenate the extra []
onto the form field name, or so that you can have an array of uploaded files. The rest is pretty standard. You can also see how I chose to output my messages related to the file constraints themselves.
<fieldset> <legend\>Media</legend> <div class="form_field"> {{ form_label(form.images) }} {{ form_widget(form.images, { 'full_name': 'kmv_ampbundle_review[images]' ~ '[]' }) }} {% if errorMessages is defined %} <ul class="error"> {% for errorMessage in errorMessages %} <li>{{ errorMessage }}</li> {% endfor %} </ul> {% endif %} </div> // Other fields ... </fieldset>
Whew! That’s it, I hope this helps you or gives you some ideas on how to set up multiple file uploading in your Symfony2 project!
15 Comments
I am sorry but every time I submit my form, the $files return null, any idea why?
Hi Hassine, do you have
enctype="multipart/form-data"
on your opening form tag?Hi i need your help please i want to implement this
Happy to help Trinita, what exactly do you need?
And what did you use to make the multiple uploader pretty? (:
You have lots of options. A quick Google search will return lots of javascript/jquery based plugins for form upload input styling. You can also make your own fairly easily. Finally, you can just style the standard input field with good results too. It all depends on the overall look of your particular site. Thanks for asking!
Hi, i am having this problem when trying to upload one file: Catchable Fatal Error: Argument 3 passed to AppBundle\Services\FileUploader::__construct() must be an instance of AppBundle\Services\Validator, instance of Symfony\Component\Validator\Validator\RecursiveValidator given, called in /home/me/Projects/csfgtwe.admin/app/cache/dev/appDevDebugProjectContainer.php on line 329 and defined
This is my services.yml
app.fileuploader:
class: AppBundle\Services\FileUploader
arguments: [“@doctrine.orm.entity_manager”, “@request_stack”, “@validator”, “@kernel”]
What version of Symfony are you running? Have you tried running
cache:clear --env=dev
in your terminal?Its solved now. It was because of my symfony2 version. Thanks!
I looking for to do the same of you but I don’t be success… Can you help me ?
In my Entity (named “Article”)
/**
* @var array
*
* @ORM\Column(name=”images”, type=”array”, nullable=true)
*/
private $images = array();
/**
* @param array $images
*/
public function setImages(array $files)
{
$this->images = $files;
return $this;
}
/**
* @return array
*/
public function getImages()
{
return $this->images;
}
ArticleType
$builder
->add(‘images’, ‘file’, array(
‘data_class’ => null,
‘attr’ => array(
‘accept’ => ‘image/*’,
‘multiple’ => ‘multiple’
)
));
Controller
if ($form->isValid()){
$files = $form->getData()->getImages();
if($files[0] != ”) {
$constraints = array(‘maxSize’=>’1M’, ‘mimeTypes’ => array(‘image/*’));
$uploadFiles = $this->get(‘articleBundle.fileuploader’)->create($files, $constraints);
}
if($uploadFiles->upload()) {
$a->setImages($uploadFiles->getFilePaths());
}
}
services.yml
services:
articlebundle.fileuploader:
class: ArticleBundle\Services\FileUploader
arguments: [ “@doctrine.orm.entity_manager”, “@request_stack”, “@validator”, “@kernel” ]
The service FileUploader is the same of you
and my view
{{ form_start(form) }}
{{ form_widget(form.images, { ‘full_name’: ‘kmv_ampbundle_review[images]’ ~ ‘[]’ }) }}
{{ form_widget(form) }}
{{ form_end(form) }}
(I don’t enderstand the “full_name”)
So, when i valid my form I get an error
at PropertyAccessor ::throwInvalidArgumentException (‘Argument 1 passed to ArticleBundle\Entity\Article::setImages() must be of the type array, null given,
Have you any solution ? THX
You’re using the wrong “full name” on the form.images twig widget. You’ll need to use your namespace. I hope that does it for you!
hi,
does your code works for symfony 3? I get this error:
Catchable Fatal Error: Argument 3 passed to AppBundle\Services\FileUploader::__construct() must be an instance of AppBundle\Services\Validator, instance of Symfony\Component\Validator\Validator\RecursiveValidator given, called in /home/me/Projects/csfgtwe.admin/app/cache/dev/appDevDebugProjectContainer.php on line 329 and defined
thanks
Hey Marouen! I am actually not 100% sure. I think there may be an issue when running on Symfony 3. In fact, I would no longer approach uploading multiple images in this way. I have planned to write a new article on how I upload images now. For now, I believe the simple fix is to pass the correct service in the services.yml file or change the type hinting in the service.
hi Kegan V,
i am trying multiple file upload on Symfony 3. but ist not working . can you make something for symfony 3.
or can we discuss online for a tutorial about it.
tndonko@gmail.de
Hi Tanguy! I do have plans to make another article on doing it in Symfony3. It is in a list of a lot of things to do. I will try to remember to email you when I have posted it. Thank you!