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!

Published: June 8, 2014 1:07 pm Categorized in:

15 Comments

  • Hassine says:

    I am sorry but every time I submit my form, the $files return null, any idea why?

  • trinita says:

    Hi i need your help please i want to implement this

  • Mauro says:

    And what did you use to make the multiple uploader pretty? (:

    • Kegan V. says:

      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!

  • Alvar says:

    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”]

  • Alvar says:

    Its solved now. It was because of my symfony2 version. Thanks!

  • michael says:

    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

    • Kegan V. says:

      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!

  • Marouen says:

    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

    • Kegan V. says:

      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.

  • tanguy says:

    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

    • Kegan V. says:

      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!

Share Your Comments

I value your privacy, your email address will not be published or shared.

This site uses Akismet to reduce spam. Learn how your comment data is processed.