14

I am making a Symfony2 application which needs to have a multiple image upload option. I have made the single file upload using the cookbook entry: How to handle File Uploads with Doctrine which works fine. I have implemented the lifecyclecallbacks for uploading and removing.

Now I need to turn this into a multiple upload system. I have read a few answers from Stack Overflow as well, but nothing seems to work.

Stack Overflow Question:

  1. Multiple file upload with Symfony2
  2. multiple file upload symfony 2

I have the following code at the moment:

File Entity:

<?php
namespace Webmuch\ProductBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\HttpFoundation\File\UploadedFile;


/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class File
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    public $path;

    /**
     * @Assert\File(maxSize="6000000")
     */
    public $file = array();

    public function __construct()
    {

    }

    /**
     * Get id
     *
     * @return integer 
     */
    public function getId()
    {
        return $this->id;
    }

    /**
     * Set path
     *
     * @param string $path
     */
    public function setPath($path)
    {
        $this->path = $path;
    }

    /**
     * Get path
     *
     * @return string 
     */
    public function getPath()
    {
        return $this->path;
    }


    public function getAbsolutePath()
    {
        return null === $this->path ? null : $this->getUploadRootDir().'/'.$this->path;
    }

    public function getWebPath()
    {
        return null === $this->path ? null : $this->getUploadDir().'/'.$this->path;
    }

    protected function getUploadRootDir()
    {
        // the absolute directory path where uploaded documents should be saved
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    protected function getUploadDir()
    {
        // get rid of the __DIR__ so it doesn't screw when displaying uploaded doc/image in the view.
        return 'uploads';
    }

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this->file) {
            // do whatever you want to generate a unique name
            $this->path[] = uniqid().'.'.$this->file->guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this->file) {
            return;
        }

        // if there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        $this->file->move($this->getUploadRootDir(), $this->path);

        unset($this->file);
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        if ($file = $this->getAbsolutePath()) {
            unlink($file);
        }
    }
}

FileController:

<?php

namespace Webmuch\ProductBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

use Webmuch\ProductBundle\Entity\File;


/**
 * File controller.
 *
 * @Route("/files")
 */
class FileController extends Controller
{
    /**
     * Lists all File entities.
     *
     * @Route("/", name="file_upload")
     * @Template()
     */
    public function uploadAction()
    {
        $file = new File();
        $form = $this->createFormBuilder($file)
            ->add('file','file',array(
                    "attr" => array(
                        "accept" => "image/*",
                        "multiple" => "multiple",
                    )
                ))
            ->getForm()
        ;

        if ($this->getRequest()->getMethod() === 'POST') {
            $form->bindRequest($this->getRequest());
                $em = $this->getDoctrine()->getEntityManager();

                $em->persist($file);
                $em->flush();

                $this->redirect($this->generateUrl('file_upload'));
        }

        return array('form' => $form->createView());
    }
}

and the upload.html.twig:

{% extends '::base.html.twig' %}

{% block body %}
<h1>Upload File</h1>

<form action="#" method="post" {{ form_enctype(form) }}>

    {{ form_widget(form.file) }} 

    <input type="submit" value="Upload" />
</form>
{% endblock %}

I don't know what to do to make this work as a multiple file upload system. I have kept the comments as they are from the tutorials I have followed so I can remember what is doing what.

UPDATE:

New Form Code:

$images_form = $this->createFormBuilder($file)
    ->add('file', 'file', array(
            "attr" => array(
                "multiple" => "multiple",
                "name" => "files[]",
            )
        ))
    ->getForm()
;

New Form Twig Code:

<form action="{{ path('file_upload') }}" method="post" {{ form_enctype(images_form) }}>

    {{ form_label(images_form.file) }}
    {{ form_errors(images_form.file) }}
    {{ form_widget(images_form.file, { 'attr': {'name': 'files[]'} }) }}

    {{ form_rest(images_form) }}
    <input type="submit" />
</form>
Community
  • 1
  • 1
rahul tripathi
  • 321
  • 2
  • 7
  • 18
  • What isn't working with this at the moment? – halfer May 04 '12 at 09:57
  • Thanks for the reply. If I select for example 5 files, only the last file is being uploaded. – rahul tripathi May 04 '12 at 12:00
  • Ah yes - your input control needs to have an individual name - since it doesn't have a name at the moment, it uses a default one for all controls. – halfer May 04 '12 at 13:16
  • I am unable to add a name to that. Please see my form code I have updated the question. I try to change the name but a default form[file] is coming. And whatever I try to add from the formBuilder is added to the label. Weird! – rahul tripathi May 08 '12 at 06:02
  • _I am unable to add a name to [the form element]_ - in what sense? Do you mean for technical reasons you can't, or that you are trying to do so but it isn't working? If the latter, have you checked the HTML to see what is produced by the Twig template? Also, if the name of `files[]` isn't working, try individual names in a loop (`file_0`, `file_1` etc). – halfer May 08 '12 at 08:55
  • I tried to add the `name` attribute using the new twig code above but instead of getting added to the widget, it gets added to the label element. I'm not sure how to add it using the for loop, I have a single form element with a `multiple="multiple"`, how can it have multiple name attributes. If you could give a code example, that would be perfect. Thanks! – rahul tripathi May 08 '12 at 10:04
  • I've never used Twig. But now you know what the problem is, looking at the Symfony2 docs should make solving it easy `:)`. – halfer May 08 '12 at 10:20

3 Answers3

15

This is a known issue as referenced on GitHub.

As they say, you should append [] to the full_name attribute in your template :

{{ form_widget(images_form.file, { 'full_name': images_form.file.get('full_name') ~ '[]' }) }}
iamdto
  • 1,372
  • 11
  • 23
  • 4
    In symfony 2.5 you may achieve it using: `{{ form_widget(images_form.file, {'full_name': images_form.file.vars.full_name ~ '[]' }) }}` – ezpn Oct 31 '14 at 11:36
  • great! this is the correct answer and it should be marked as valid. – Gerardo Feb 03 '15 at 13:57
  • Thanks for the github issue link. It really helped me out with my validation problem. I added another solution as an answer also, which also works perfectly. – func0der Mar 06 '15 at 14:23
5

I do not know if that is possible with the annotation syntax. So I am going to write it in in plain PHP in the controller

$images_form = $this->createFormBuilder($file)
    ->add('file', 'file', array(
        'constraints' => array(
            new NotBlank(), // Makes sure it is filled at all.
            new All(array( // Validates each an every entry in the array that is uploaded with the given constraints.
                'constraints' => array(
                    new File(array(
                        'maxSize' => 6000000
                    )),
                ),
            )),
        ),
        'multiple' => TRUE,
    ))
    ->getForm();

This should work perfectly since Symfony 2.4. Before that you would have to put the multiple attribute in the attr key like you already did.

As I said, you have to to make this work with annotations. It might work but could be less readable if you have to put it all in one line.

Have fun ;)

func0der
  • 2,192
  • 1
  • 19
  • 29
  • 1
    regarding the annotations: this is given as an example on the symfony website. http://symfony.com/doc/current/reference/constraints/All.html `class User { /** * @Assert\All({ * @Assert\NotBlank, * @Assert\Length(min = 5) * }) */ protected $favoriteColors = array(); }` – nexana Jul 20 '16 at 07:28
3

I had the same problem recently, followed the suggestions here, but got an error, because the validator 'file' can't handle arrays.

So I had to write my own validator, which can handle multiple files. For that, I followed this tutorial from Symfony.com, copy/pasted the code from validator 'file' in it, encapsulated it with a foreach loop and changed the variables as needed.

If you do this, you can use it for $file in your entity.

Core Xii
  • 6,270
  • 4
  • 31
  • 42
tjwelde
  • 353
  • 1
  • 2
  • 9