@ManHunter

Загрузка нескольких файлов в Symfony?

Здравствуйте.
Начинаю разбираться с framework-ом Symfony.
Проект создан на Symfony 3.0.4.

Создал две сущности:
  • Product
  • Image


Листинг Product.php:
<?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:
<?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 ...


В поле image (потом переименую в более подходящее имя) таблицы Image буду хранить имя картинки, чтобы в будущем по добавляя путь до папки с загруженными изображениями, формировался полный путь.

Схема БД:
746f72ee0c.jpg

На данный момент у меня создана форма ProductType.php:

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';
    }
}


Теперь в неё нужно добавить поле с загрузкой файлов.
Я искал примеры в документации, но там был пример с одним файлом и ещё один, но там тоже не объяснялось как сделать множественную загрузку и другие детали.

У меня есть предположение, что можно добавить к форме Product ещё поле:
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();
    }

но это же не сработает.

Вообщем нужна ваша помощь, уже не второй день ищу ответ на мой вопрос.

UPDATED

Нашёл на stackoverflow то, что мне нужно.

Изменения:

Image.php - добавил поля name, path и file (name, path хранятся в БД, file - нет):
/**
 * 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 - в аннотации добавил каскадную вставку в таблицу Image, и добавил метод addImage(Image):
/**
     * @ORM\OneToMany(targetEntity="Image", mappedBy="product", cascade={"persist"})
     */
       private $images;

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

        $image->setProduct($this);

        return $this;
    }


Далее создал форму для Image - ImageType.php:
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class)
            ->add('file', FileType::class)
            ->getForm();
    }


Добавил поле images в форму ProductType.php:
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();
    }

тип формы images - колеекция состоящая из моего ImageType.

Код Action-а в контроллере:
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()
        ));
    }


Это всё.
Теперь к проблемам.
При отправке заполненной формы выдает ошибку, говорящая о том, что поле name таблицы image не может быть 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

Что странно т.к. я ввел name при заполнении формы.
Ещё я не знаю как и что мне делать когда в конроллер придёт ответ с заполненной формой. Перед тем как записать всё в БД нужно:
  1. Сохранить файл
  2. Записать в path путь
  3. Записать в name имя файла

(это не считая всяких проверок на существование файла с таким же именем и прочее). - Вот это как сделать не знаю.
Вторая проблема с такой реализацией в том, что я не могу выбрать сразу несколько файлов. Мне придется в addProductAction инициализировать несколько новых файлов:
$image1 = new Image();
$product->addImage($image1);

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

Но тогда пользователь сможет загрузить только заданное кол-во изображений.
Скорее всего эту проблему можно решить динамическим добавлением форм при помощи javascript, но я хотел бы иметь одну форму, а у пользователя была возможность выделить несколько файлов за раз.
Уверен что каждый кто работал с symfony знает как это делается. Помогите пожалуйста.
  • Вопрос задан
  • 1893 просмотра
Решения вопроса 1
@ManHunter Автор вопроса
Сделал загрузку без использования форм.
В шаблоне добавил:
<input type="file" name="files[]" multiple>
В Action получаю массив с файлами и создаю объекты Image передавая файл в setFile(file), а затем добавляю объект Image в $product->images:
$images = $request->files->get('files');
foreach($images as $file) {
   $image = new Image();
   $image->setFile($file);
   $product->addImage($image);
}

Дальше всё работает как тут: symfony.com/doc/current/cookbook/doctrine/file_upl...
с использованием Lifecycle Callbacks.
Ответ написан
Комментировать
Пригласить эксперта
Ответы на вопрос 1
voronkovich
@voronkovich
В ваше коде есть несколько проблем. Возможно, если вы устраните их, то все заработает.

1. У вас нет обработки загрузки файла. Его нужно сохранять вручную.
Пример: https://github.com/symfony/symfony-demo/pull/286/files

В контроллере нужно обработать все загруженные файлы следующим образом:
foreach ($product->getImages() as $image) {
    $uploadedFile = $image->getFile();
    $uploadedFile->move('uploads/', $uploadedFile->getClientOriginalName());
    $image->setPath('uploads/' . $uploadedFile->getClientOriginalName());
}

И почитайте внимательно symfony.com/doc/current/cookbook/doctrine/file_upl...
Также посмотрите api.symfony.com/3.0/Symfony/Component/HttpFoundati...

2. Во всех формах укажите data_class, и тогда не нужно будет создавать экземпляр Image вручную.
См. symfony.com/doc/current/reference/forms/types/form...

3. Вам не следует делать вызов метода getForm() внутри buildForm

4. Начиная с симфони 2.8 объявление метода getName не нужно. Теперь формы идентифицируются по полному имени класса.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы