M
M
ManHunter2016-05-12 09:41:52
symfony
ManHunter, 2016-05-12 09:41:52

Uploading multiple files in Symfony?

Hello.
I'm getting started with the Symfony framework.
The project was created on Symfony 3.0.4.
Created two entities:

  • product
  • Image

Product.php listing:
<?php

namespace AppBundle\Entity;

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
     * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
     */
    private $category;

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

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

    /**
     * @var string
     *
     * @ORM\Column(name="price", type="decimal", precision=10, scale=0)
     */
    private $price;

    /**
     * @var \DateTime
     *
     * @ORM\Column(name="date", type="datetime")
     */
    private $date;

    /**
     * @ORM\OneToMany(targetEntity="Image", mappedBy="product")
     */
    private $images;

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

// getters & setters ...

Image.php listing:
<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

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

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

    /**
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="images")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id")
     */
    private $product;

  // getters & setters ...

In the image field (later I will rename it to a more appropriate name) of the Image table, I will store the name of the image, so that in the future, by adding the path to the folder with downloaded images, the full path will be formed.
DB Schema:
746f72ee0c.jpg
At the moment I have a ProductType.php form:
class ProductType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('description', TextareaType::class)
            ->add('price', NumberType::class)
            ->add('category', EntityType::class, array(
                'class' => 'AppBundle\Entity\Category',
                'choice_label' => 'category'
            ))
            ->getForm();
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'register_class' => 'AppBundle:Entity:Product'
        ));
    }

    public function getName()
    {
        return 'app_bundle_product_type';
    }
}

Now you need to add a file upload field to it.
I looked for examples in the documentation, but there was an example with one file and another one, but it also did not explain how to do multiple uploads and other details.
I have a suggestion that you can add another field to the Product form:
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('description', TextareaType::class)
            ->add('price', NumberType::class)
            ->add('category', EntityType::class, array(
                'class' => 'AppBundle\Entity\Category',
                'choice_label' => 'category'
            ))
           ->add('images', ColeectionType:class) // ДОБАВЛЕННОЕ ПОЛЕ
            ->getForm();
    }

but that won't work.
In general, I need your help, I have been looking for an answer to my question for more than a day.
UPDATED
Found what I need on stackoverflow .
Changes:
Image.php - added name, path and file fields (name, path are stored in the database, file - not):
/**
 * Image
 *
 * @ORM\Table(name="image")
 * @ORM\Entity(repositoryClass="AppBundle\Repository\ImageRepository")
 */
class Image
{
    /**
     * @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=50)
     */
    private $name;

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

    /**
     * @ORM\ManyToOne(targetEntity="Product", inversedBy="images")
     * @ORM\JoinColumn(name="product_id", referencedColumnName="id")
     */
    private $product;

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

    // геттеры и сеттеры

Product.php - added a cascading insert to the Image table in the annotation, and added the addImage(Image) method:
/**
     * @ORM\OneToMany(targetEntity="Image", mappedBy="product", cascade={"persist"})
     */
       private $images;

       public function addImage(Image $image) {
        $this->images[] = $image;

        $image->setProduct($this);

        return $this;
    }

Next, I created a form for Image - ImageType.php:
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('file', FileType::class)
            ->getForm();
    }

Added the images field to the ProductType.php form:
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('description', TextareaType::class)
            ->add('price', NumberType::class)
            ->add('category', EntityType::class, array(
                'class' => 'AppBundle\Entity\Category',
                'choice_label' => 'category'
            ))
            ->add('images', CollectionType::class, array(
                'entry_type' => ImageType::class,
                'allow_add' => true,
                'by_reference' => false,
                'allow_delete' => true,
                'prototype' => true
            ))
            ->getForm();
    }

the form type images is a collection of my ImageType.
Action code in the controller:
public function addProductAction(Request $request)
    {
        $product = new Product();

        $image = new Image();
        $product->addImage($image);

        $form = $this->createForm(ProductType::class, $product);
        $form->handleRequest($request);

        if ($form->isValid()) {
            $product->setDate(new \DateTime());

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

            return $this->redirectToRoute('admin_products');
        }

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

It's all.
Now to the problems.
When submitting the completed form, it gives an error saying that the name field of the image table cannot be null:
An exception occurred while executing 'INSERT INTO image (name, path, product_id) VALUES (?, ?, ?)' with params [null, "initial", 12]:
SQLSTATE[23000]: Integrity constraint violation: 1048 Column 'name ' cannot be null

Which is strange because i entered name when filling out the form.
I also don’t know how and what to do when the controller receives a response with a completed form. Before writing everything to the database, you need to:
  1. Save file
  2. Write path to path
  3. Write to name file name

(this is not counting any checks for the existence of a file with the same name, etc.). - I don't know how to do this.
The second problem with this implementation is that I can't select multiple files at once. I will have to initialize several new files in addProductAction:
$image1 = new Image();
$product->addImage($image1);

$image2 = new Image();
$product->addImage($image2);

But then the user will be able to upload only the specified number of images.
This problem can most likely be solved by dynamically adding forms using javascript, but I would like to have one form, and the user could select several files at a time.
I'm sure that everyone who has worked with symfony knows how it's done. Help me please.

Answer the question

In order to leave comments, you need to log in

2 answer(s)
M
ManHunter, 2016-05-14
@ManHunter

Made the upload without using forms.
In the template I added:
In the Action, I get an array with files and create Image objects by passing the file to setFile (file), and then I add the Image object to $product->images:

$images = $request->files->get('files');
foreach($images as $file) {
   $image = new Image();
   $image->setFile($file);
   $product->addImage($image);
}

Then everything works like this: symfony.com/doc/current/cookbook/doctrine/file_upl...
using Lifecycle Callbacks.

V
voronkovich, 2016-05-13
@voronkovich

There are several problems in your code. Perhaps if you eliminate them, then everything will work.
1. You don't have file upload processing. It must be saved manually.
Example: https://github.com/symfony/symfony-demo/pull/286/files
In the controller, all uploaded files should be handled like this:

foreach ($product->getImages() as $image) {
    $uploadedFile = $image->getFile();
    $uploadedFile->move('uploads/', $uploadedFile->getClientOriginalName());
    $image->setPath('uploads/' . $uploadedFile->getClientOriginalName());
}

And read carefully symfony.com/doc/current/cookbook/doctrine/file_upl...
Also see api.symfony.com/3.0/Symfony/Component/HttpFoundati
... create an Image instance manually.
See symfony.com/doc/current/reference/forms/types/form...
3. You should not make a call to the getForm() method inside buildForm
4. As of symfony 2.8, a getName method declaration is not needed. Forms are now identified by the fully qualified class name.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question