Multiple file upload with Symfony2

2019-01-08 10:50发布

问题:

I'm trying to upload multiple files via a form, but I can only upload one file at a time, the last one I mark in the browser. Is there a way to upload more images with Symfony2 using a simple form? Here is the twig template of the form I'm using to be able to mark more than one file:

{{ form_widget(form.post_image, { 'attr': {'multiple': 'multiple' }}) }} 

回答1:

No extra classes needed (except the gallery_manger service but the issue you describe happens before...)

I don't really know what's wrong. Check for your template (maybe wrong enctype... or name attr missmatching)

first try to do a single file upload, check the documentation:

  1. file Field Type
  2. How to handle File Uploads with Doctrine

Once it works, you have to edit some lines.

  1. add input file multiple attribute.
  2. append [] at the end of the input file name attribute (mine is create...[] because my form class name is create, if your is createType it will be createType...[])
  3. init $files as an array.

Copy/paste your code here.



回答2:

Ok binding issue solved (enctype syntax error) : i'll give you the code i use. maybe it will help...

I have a Gallery Entity

class Gallery
{
    protected $id;
    protected $name;
    protected $description;
    private $urlName;
    public $files; // the array which will contain the array of Uploadedfiles

    // GETTERS & SETTERS ...

    public function getFiles() {
        return $this->files;
    }
    public function setFiles(array $files) {
        $this->files = $files;
    }

    public function __construct() {
        $files = array();
    }
}

I have a form class that generate the form

class Create extends AbstractType {

    public function buildForm(FormBuilder $builder, array $options) {

        $builder->add('name','text',array(
            "label" => "Name",
            "required" => TRUE,
        ));
        $builder->add('description','textarea',array(
            "label" => "Description",
            "required" => FALSE,
        ));
        $builder->add('files','file',array(
            "label" => "Fichiers",
            "required" => FALSE,
            "attr" => array(
                "accept" => "image/*",
                "multiple" => "multiple",
            )
        ));
    }
}

Now in the controller

class GalleryController extends Controller
{
    public function createAction() {
        $gallery = new Gallery();
        $form = $this->createForm(new Create(), $gallery);
        // Altering the input field name attribute
        $formView = $form->createView();
        $formView->getChild('files')->set('full_name', 'create[files][]');

        $request = $this->getRequest();
        if($request->getMethod() == "POST")
        {
            $form->bindRequest($request);

            // print "<pre>".print_r($gallery->getFiles(),1)."</pre>";
            if($form->isValid())
            {
                // Do what you want with your files
                $this->get('gallery_manager')->save($gallery);
                return $this->redirect($this->generateUrl("_gallery_overview"));
            }
        }

        return $this->render("GalleryBundle:Admin:create.html.twig", array("form" => $formView));
    }
}

Hope this help...

NB: If someone know a better way to alter this f** name attribute, maybe in the FormView class or by declaring a new field type, feel free to show us your method...



回答3:

All the suggestions I've found here are workarounds for the real situation.

In order to be able to have multiple attachments, you should use form collection.

Quote from the documentation:

In this entry, you'll learn how to create a form that embeds a collection of many other forms. This could be useful, for example, if you had a Task class and you wanted to edit/create/remove many Tag objects related to that Task, right inside the same form.

http://symfony.com/doc/2.0/cookbook/form/form_collections.html

Example case: You have a document, which form is specified by DocumentType. The document must have multiple attachments, which you can have by defining AttachmentType form and adding it as a collection to the DocumentType form.



回答4:

use this methode :

$form = $this->createFormBuilder()
->add('attachments','file', array('required' => true,"attr" => array(
  "multiple" => "multiple",
  )))
->add('save', 'submit', array(
  'attr' => array('class' => 'btn btn-primary btn-block btn-lg'),
  'label' => 'save'
  ))
->getForm();

then you add [] to the name of your input via jQuery :

<input id="form_attachments" name="form[attachments]" required="required" multiple="multiple" type="file">

jQuery code :

 <script>
  $(document).ready(function() {
    $('#form_attachments').attr('name',$('#form_attachments').attr('name')+"[]");
  });
</script>


回答5:

For sf > 2.2 : In you form type class, add this overrided method :

public function finishView(FormView $view, FormInterface $form, array $options) {
    $view->vars['form']->children['files']->vars['full_name'] .= '[]';
}


回答6:

Note that i try to do the same thing in sf2 using this syntax:

In the controller:

public function stuffAction() {

$form = $this->createFormBuilder()
                ->add('files','file',array(
                    "attr" => array(
                        "accept" => "image/*",
                        "multiple" => "multiple",
                    )
                ))
                ->getForm();

        $formView = $form->createView();
        $formView->getChild('files')->set('full_name', 'form[files][]');

// name param (eg 'form[files][]') need to be the generated name followed by []
// try doing this : $formView->getChild('files')->get('full_name') . '[]'


        $request = $this->getRequest();
        if($request->getMethod() == "POST") {

            $form->bindRequest($request);

            $data = $form->getData();
            $files = $data["files"];

            // do stuff with your files
        }

    }

    return $this->render('Bundle:Dir:index.html.twig',array("form" => $formView));
}

$files will be an array of uploaded files...

Calling $form->createView() to alter the name attribute is certainly not the best way / cleanest way to do it but it's the only one i found that keeps the csrf functionality working, because altering the name attribute in a twig template makes it invalid...

Now I still have an issue using a form class which generate the form, I don't know why during the binding of the form data & object attached to the form my array of uploaded files is transformed in array of (file) name ???



回答7:

Here is easy example to upload multiple files. I have similar problem with upload files.

https://github.com/marekz/example_symfony_multiply_files_example

For symfony 3.*

First: Both form declatartion:

    <?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use AppBundle\Form\FilesType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;

class UserType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
                ->add('name')
                ->add('lastName')
                ->add('files', CollectionType::class,array(
                    'entry_type' => FilesType::class,
                    'allow_add' => true,
                    'by_reference' => false,
                ))
                ;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\User'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_user';
    }


}


    <?php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class FilesType extends AbstractType
{
    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add('file');
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Files'
        ));
    }

    /**
     * {@inheritdoc}
     */
    public function getBlockPrefix()
    {
        return 'appbundle_files';
    }


}

Now, my entities:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;

/**
 * User
 *
 * @ORM\Table(name="user")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\UserRepository")
 */
class User {

    /**
     * @var int
     *
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

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

    /**
     * @ORM\ManyToMany(targetEntity="Files", cascade={"persist"})
     */
    private $files;

    function __construct() {
        $this->files = new ArrayCollection();
    }

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

    /**
     * Set name
     *
     * @param string $name
     *
     * @return User
     */
    public function setName($name) {
        $this->name = $name;

        return $this;
    }

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

    /**
     * Set lastName
     *
     * @param string $lastName
     *
     * @return User
     */
    public function setLastName($lastName) {
        $this->lastName = $lastName;

        return $this;
    }

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

    /**
     * Get files
     * 
     * @return ArrayCollection
     */
    function getFiles() {
        return $this->files;
    }

    /**
     * Set files
     * @param type $files
     */
    function setFiles($files) {
        $this->files = $files;
    }
}

    <?php

namespace AppBundle\Entity;

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

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

    /**
     * @var string
     *
     * @ORM\Column(name="file", type="string", length=255, unique=true)
     * @Assert\NotBlank(message="Please, upload the product brochure as a PDF file.")
     * @Assert\File(mimeTypes={ "application/pdf" })
     */
    private $file;

    /**
     *
     * @return Files
     */
    function getUser() {
        return $this->user;
    }

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

    /**
     * Set file
     *
     * @param string $file
     *
     * @return Files
     */
    public function setFile($file)
    {
        $this->file = $file;

        return $this;
    }

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

Finaly, Symfony Controller:

/**
 * Creates a new user entity.
 *
 * @Route("/new", name="user_new")
 * @Method({"GET", "POST"})
 */
public function newAction(Request $request) {
    $user = new User();
    $form = $this->createForm('AppBundle\Form\UserType', $user);
    $form->handleRequest($request);

    if ($form->isSubmitted() && $form->isValid()) {

        $attachments = $user->getFiles();

        if ($attachments) {
            foreach($attachments as $attachment)
            {
                $file = $attachment->getFile();

                var_dump($attachment);
                $filename = md5(uniqid()) . '.' .$file->guessExtension();

                $file->move(
                        $this->getParameter('upload_path'), $filename
                );
                var_dump($filename);
                $attachment->setFile($filename);
            }
        }

        $em = $this->getDoctrine()->getManager();
        $em->persist($user);
        $em->flush();

        return $this->redirectToRoute('user_show', array('id' => $user->getId()));
    }

    return $this->render('user/new.html.twig', array(
                'user' => $user,
                'form' => $form->createView(),
    ));
}


回答8:

You need to alter the input file name attribute which need to map an array.

<input type="file" name="name[]" multiple />


回答9:

Methods getChild and set() were removed in 2.3. Instead of this you should use children[] and vars properties

before:

$formView->getChild('files')->set('full_name', 'form[files][]');

after:

$formView->children['files']->vars = array_replace($formView->children['files']->vars, array('full_name', 'form[files][]'));


回答10:

Symfony introduced 'multiple' option to file field type in symfony 2.5

$builder->add('file', 'file', array('multiple' => TRUE));


回答11:

What would happen if there would be some validation errors? Will Symfony Form repost multiple file upload field. Because I tried it and I think for this purpose you need to use collection of file fields. Than symfony form must render all fields have added before correctly.