Symfony3: ArrayCollection имеет только последний добавленный элемент

У меня есть объект Product. Мой продукт может иметь несколько названий на разных языках. Имя на французском, имя на английском и т. д. Я не хочу использовать автоматический перевод.

Пользователь должен будет написать названия в форме продукта и выбрать соответствующий язык. Он может добавить столько имен, сколько захочет, благодаря кнопке «Добавить».

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

Итак, ProductType — это моя основная форма, а ProductNameType — моя форма «коллекции».

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

На данный момент все работает хорошо. Мой addAction() в порядке, товар и соответствующие названия сохранены в базе данных.

Но у меня возникла проблема с editAction(), когда я показываю предварительно заполненную форму. В моем массиве коллекций присутствует только последнее добавленное название продукта...

Я не понимаю, что я делаю неправильно. Имена есть в моей базе данных, товар тоже, так почему я получаю только фамилию в ArrayCollection?

Сущность Product.php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @ORM\Table(name="modele")
 * @ORM\Entity(repositoryClass="ProductRepository")
 * @UniqueEntity(fields="code", message="Product code already exists")
 */
class Product
{
    /**
     * @ORM\Column(name="Modele_Code", type="string", length=15)
     * @ORM\Id
     * @Assert\NotBlank()
     * @Assert\Length(max=15, maxMessage="The code cannot be longer than {{ limit }} characters")
     */
    private $code;

    /**
     * @ORM\OneToMany(targetEntity="ProductNames", mappedBy="product", cascade={"persist", "remove"})
     */
    private $names;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->names = new ArrayCollection();
    }

    /**
     * Set code
     *
     * @param string $code
     *
     * @return Product
     */
    public function setCode($code)
    {
        $this->code = $code;

        return $this;
    }

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

    /**
     * Get names
     *
     * @return ArrayCollection
     */
    public function getNames()
    {
      return $this->names;
    }

    /**
     * Add names
     *
     * @param ProductNames $names
     *
     * @return Product
     */
    public function addName(ProductNames $names)
    {
        $names->setCode($this->getCode());
        $names->setProduct($this);

        if (!$this->getNames()->contains($names)) {
            $this->names->add($names);
        }

        return $this;
    }

    /**
     * Remove names
     *
     * @param ProductNames $names
     */
    public function removeName(ProductNames $names)
    {
        $this->names->removeElement($names);
    }
}

Сущность ProductNames.php

namespace AppBundle\Entity;

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

/**
 * @ORM\Table(name="modele_lib")
 * @ORM\Entity(repositoryClass="ModelTextsRepository")
 * @UniqueEntity(fields={"code","language"}, message="A name in this language already exists for this product")
 */
class ProductNames
{
    /**
     * @ORM\Column(name="Modele_Code", type="string", length=15)
     * @ORM\Id
     */
    private $code;

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

    /**
     * @ORM\Column(name="Langue_Code", type="string", length=2)
     */
    private $language;

    /**
     * @ORM\Column(name="Modele_Libelle", type="string", length=50)
     * @Assert\NotBlank()
     */
    private $name;

    /**
     * Set code
     *
     * @param string $code
     *
     * @return ProductNames
     */
    public function setCode($code)
    {
        $this->code = $code;

        return $this;
    }

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

     /**
      * Set product
      *
      * @param Product $product
      *
      * @return ProductNames
      */
    public function setProduct(Model $product)
    {
        $this->product = $product;

        return $this;
    }

    /**
     * Get product
     *
     * @return Product
     */
    public function getProduct()
    {
        return $this->product;
    }

    /**
     * Set language
     *
     * @param string $language
     *
     * @return ProductNames
     */
    public function setLanguage($language)
    {
        $this->language = $language;

        return $this;
    }

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

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

        return $this;
    }

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

Форма ProductType.php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
// use Doctrine\ORM\EntityRepository;

class ProductType extends AbstractType
{

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

        $recordId = $options['data']->getCode();    // product code

        // default options for names
        $namesOptions = array(
            'entry_type'   => ProductNamesType::class,
            'entry_options' => array('languages' => $options['languages']),
            'allow_add'     => true,
            'allow_delete'  => true,
            'prototype'     => true,
            'label'         => false,
            'by_reference' => false
        );

        // case edit product
        if (!empty($recordId)) {
            $namesOptions['entry_options']['edit'] = true;
        }

        $builder
            ->add('code',       TextType::class, array(
                'attr'              => array(
                    'size'          => 15,
                    'maxlength'     => 15,
                    'placeholder'   => 'Ex : LBSKIN'
                ),
            ))

            ->add('names',      CollectionType::class, $namesOptions)

            ->add('save',       SubmitType::class, array(
                'attr'          => array('class' => 'button-link save'),
                'label'         => 'Validate'
            )
        );

        // Edit case : add delete button
        if (!empty($recordId)) {
            $builder->add('delete', SubmitType::class, array(
                'attr'      => array('class' => 'button-link delete'),
                'label'     => 'Delete'
            ));
        }
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\Product',
            'languages'  => null
        ));
    }
}

Форма ProductNamesType.php

namespace AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Ivory\CKEditorBundle\Form\Type\CKEditorType;
use Doctrine\ORM\EntityRepository;

class ProductNamesType extends AbstractType
{

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

        // Language codes list
        $choices = array();
        foreach ($options['languages'] as $lang) {
            $code = $lang->getCode();
            $choices[$code] = $code;
        }

        $builder
            ->add('name',           TextType::class)

            ->add('language',       ChoiceType::class, array(
                'label'             => 'Language',
                'placeholder'       => '',
                'choices'           => $choices
            ))
        ;
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'AppBundle\Entity\ProductNames',
            'languages'  => null,
            'edit'       => false
        ));
    }

}

ProductController.php (см. editAction, чтобы найти мою проблему).

Если я печатаю $form->getData() или $product->getNames() в addAction() после отправки формы, я получаю все свои данные, для меня все в порядке.

namespace AppBundle\Controller;

use AppBundle\Form\ProductType;
use AppBundle\Entity\Product;
use AppBundle\Entity\ProductNames;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Doctrine\Common\Collections\ArrayCollection;

class ProductController extends Controller
{

   /**
    * @Route("/products/add", name="product_add")
    */
    public function addAction(Request $request) {

      // build the form
      $em = $this->getDoctrine()->getManager();
      $languages = $em->getRepository('AppBundle:Language')->findAllOrderedByCode();


      $product = new Product();

      $form = $this->createForm(ProductType::class, $product, array(
            'languages' => $languages
        ));

      // handle the submit
      $form->handleRequest($request);
      if ($form->isSubmitted() && $form->isValid()) {

            // save the product
            $em->persist($product);

            foreach($product->getNames() as $names){
                $em->persist($names);
            }

            $em->flush();

            /*** here, everything is working ***/

            // success message
            $this->addFlash('notice', 'Product has been created successfully !');

            // redirection
            return $this->redirectToRoute('product');
      }

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

   /**
    * @Route("/products/edit/{code}", name="product_edit")
    */
   public function editAction($code, Request $request) {

      // get product from database
      $em = $this->getDoctrine()->getManager();
      $product = $em->getRepository('AppBundle:Product')->find($code);
      $languages = $em->getRepository('AppBundle:Language')->findAllOrderedByCode();

      // product doesn't exist
      if (!$product) {
         throw $this->createNotFoundException('No product found for code '. $code);
      }

        $originalNames = new ArrayCollection();

        /*** My PROBLEM IS HERE ***/
        // $product->getNames() returns only one name : the last added
        foreach ($product->getNames() as $names) {
           $originalNames->add($names);
        }

        // My form shows only one "name block" with the last name added when the user created the product.

      // build the form with product data
      $form = $this->createForm(ProductType::class, $product, array(
            'languages' => $languages
        ));

      // form POST
      $form->handleRequest($request);
      if ($form->isSubmitted() && $form->isValid()) {

            // ...
      }

      // show form
      return $this->render('products/form.html.twig', array(
         'form'      => $form->createView(),
         'product_code' => $code
      ));
   }
}

person Eve    schedule 09.05.2016    source источник


Ответы (1)


Проблема может быть связана с вашей сущностью ProductNames. Вы отметили code как первичный ключ (с @ORM\Id), Product будет иметь несколько ProductNames, и все они не могут иметь code в качестве первичного ключа, поскольку первичный ключ должен быть уникальным. Я бы предложил использовать составной первичный ключ, добавив аннотацию @ORM\Id к языку.

class ProductNames
{
    /**
     * @ORM\Column(name="Modele_Code", type="string", length=15)
     * @ORM\Id
     */
    private $code;

    /**
     * @ORM\Column(name="Langue_Code", type="string", length=2)
     * @ORM\Id
     */
    private $language;

    // ...
}

Вам придется обновить/пересоздать базу данных, чтобы составной ключ вступил в силу.

Надеюсь это поможет.

person Akash    schedule 09.05.2016
comment
Ты прав ! На самом деле мне нужно создать интерфейс с Symfony, который использует уже существующую базу данных. И в этой базе данных таблица modele_lib (= ProductNames) имеет двойной первичный ключ: код продукта + код языка. Я забыл написать это в своих аннотациях, спасибо, я собираюсь протестировать его, чтобы увидеть, исправит ли он мою проблему с ArrayCollection. - person Eve; 10.05.2016
comment
Спасибо большое, ты гений!! Теперь работает ;-) - person Eve; 10.05.2016