@e-hot

При редактировании коллекции сущностей не сохраняются новые сущности — почему?

Суть проблемы:
есть сущность Test:
<?php
namespace Acme\AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Acme\AppBundle\Entity\Breakagedemand;
/**
 * Test
 * @ORM\Table(name="test")
 * @ORM\Entity
 */
class Test {
    /**
     * @var integer
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue
     */
    private $id;
    ...

    /********************************** collection of breakagedemand *************************************/
    /**
     * @ORM\OneToMany(targetEntity="Acme\AppBundle\Entity\Breakagedemand", mappedBy="test", cascade={"persist"})
     */
    private $collection;
    
    public function __construct()
    {
        $this->collection = new ArrayCollection();
    }
    
    public function getCollection()
    {
        return $this->collection;
    }
    
    public function addCollection( Breakagedemand $_item )
    {
        $this->collection->add( $_item );
        $_item->addTest( $this );
    }
    
    public function removeCollection( $_item )
    {
        $this->collection->removeElement( $_item );
    }
    /********************************** collection of breakagedemand *************************************/
    ...
}

Объект с конкретным id (код из контроллера):
$em = $this->getDoctrine()->getManager();
        $repo_test = $em->getRepository('Acme\AppBundle\Entity\Test');
        $test = $repo_test->findOneBy( array( 'breakagedemands' => $id ) );
       ...

формирует соответствующие данные в форму коллекции TestType:
...

class TestType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder ->add( 'collection', 'collection', array(
                                                        'type' => new BreakagedemandType(),
                                                        'options' => array(
                                                            'required'  => false
                                                        ),
                                                        'prototype' => true,
                                                        'allow_add' => true,
                                                        'allow_delete' => true,
                                                        'by_reference' => false,
                                                        'label' => 'Поломки',
                                                        'required' => true
                    ) );
    }

    ...
}

Коллекция в этой форме формируется внедренной формой BreakagedemandType:
...
class BreakagedemandType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder->add( 'posrepairs_id' , 'hidden')
                ->add( 'isrepeated' , 'hidden')
                ->add('posbreakage_id', 'entity', array( 
                                                        'class' => 'Acme\AppBundle\Entity\Breakage',
                                                        'property' => 'name',
                                                        'label' => 'Поломка: '
                                                    ) 
                    );
    }

    ...
}

Данные в форму BreakagedemandType поступают из сущности Breakagedemand:
/**
 * Breakagedemand
 * @ORM\Table(name="breakagedemand")
 * @ORM\Entity
 */
class Breakagedemand {
    /**
     * @var integer
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue
     */
    private $id;
    ...
    /********************************* posbreakages *************************************/
    /**
     * @var integer
     * @ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\Breakage", inversedBy="breakagedemands", cascade={"persist"})
     * @ORM\JoinColumn(name="posbreakage_id", referencedColumnName="id")
     */
    private $posbreakage_id;
    
    /**
     * Set posbreakage_id
     * @param integer $posbreakageId
     * @return Breakagedemand
     */
    public function setPosbreakageId( $posbreakageId )
    {
        $this->posbreakage_id = $posbreakageId;
        return $this;
    }
    
    /**
     * Get posbreakage_id
     * @return integer 
     */
    public function getPosbreakageId()
    {
        return $this->posbreakage_id;
    }
    /********************************* posbreakages *************************************/
    
    /********************************* test collection *************************************/
    /**
     * @ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\Test", inversedBy="collection", cascade={"persist"})
     * @ORM\JoinColumn(name="posrepairs_id", referencedColumnName="breakagedemand_id")
     */
    private $test;

    public function __construct()
    {
        $this->test = new ArrayCollection();
    }
    
    public function getTest()
    {
        return $this->test;
    }
    
    public function addTest( $_item )
    {  
        if ( !$this->test->contains( $_item ) ) 
        {
            $this->test->add( $_item );
            $_item->addCollection( $this );
        }
    }
    /********************************* test collection *************************************/
}

А эта сущность Breakagedemand соответственно связана с сущностью Breakage (суть сущности Breakage в сути вопроса не важна). В итоге получаю такую форму:
c86c7c5da40940de88158e82ed10a318.png
По разделам руководства:
symfony.com/doc/2.3/reference/forms/types/collecti...
symfony.com/doc/2.3/cookbook/form/form_collections...
symfony.com/doc/2.3/cookbook/form/form_collections...
добавил в twig код js с jquery, а так же рекомендуемый код в контроллер (обозначен как "код для удаления связей между коллекцией и сущностью"):
...
    public function testCollectionAction( $id, Request $request ){
        $em = $this->getDoctrine()->getManager();
        $repo_test = $em->getRepository('Acme\AppBundle\Entity\Test');
        $test = $repo_test->findOneBy( array( 'breakagedemands' => $id ) );

        /* код для удаления связей между коллекцией и сущностью */    
            $originalcols = new ArrayCollection();
            foreach ( $test->getCollection() as $col ) {
                $originalcols->add( $col );
            }
        /* код для удаления связей между коллекцией и сущностью */

        $form = $this->createForm( new TestType(), $test );
        $form->handleRequest( $request );

        if ($form->isValid()) {
            /* код для удаления связей между коллекцией и сущностью */
                foreach ( $originalcols as $col ) {
                    if (false === $test->getCollection()->contains( $col )) {
                        $em->persist( $col );
                        $em->remove( $col );
                    }
                }
            /* код для удаления связей между коллекцией и сущностью */
               
            $em->persist( $test );
            $em->flush();
        }
        
        return $this->render('AcmeAppBundle:Forms:collection.html.twig', array(
                                                                                    'form' => $form->createView(),
                                                                                ) 
                            );
    }

В полученной форме в итоге все очень здорово редактируется (меняются типы поломок) с сохранением в БД и так же удаляются - в БД это все тоже отражается. Но не могу победить проблему добавления нового объекта в коллекцию и соответственно в БД. Сам процесс добавления:
e492f1f216134b64a2e864f108ee356b.png
и ошибка:
Found entity of type Doctrine\Common\Collections\ArrayCollection on association Acme\AppBundle\Entity\Breakagedemand#test, but expecting Acme\AppBundle\Entity\Test
Я понимаю, что доктрина ждет Breakagedemand, а подается Test.

Посмотрите, пожалуйста, код свежим взглядом, а то я, скорее всего, уже не замечаю какой-нить бяки в коде, скорее всего в маппинге связей. При добавлении объекта в коллекцию я проверяю состояние имеющихся сущностей через $em->getUnitOfWork()->getEntityState( $_item ) - говорит, что (это естественно) все имеющиеся объекты MANAGED, а добавленная - NEW.

Заранее спасибо за помощь.
  • Вопрос задан
  • 311 просмотров
Пригласить эксперта
Ответы на вопрос 2
@jaxel
Ох, ну и накуролесили вы тут. Как оно вощбще работает?))

Если делать вашим методом, то сущность Test должна выглядеть примерно так:
/**
 * Test
 * @ORM\Table(name="test")
 * @ORM\Entity
 */
class Test {
    /**
     * @var integer
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * @ORM\OneToMany(targetEntity="Acme\AppBundle\Entity\Breakagedemand", mappedBy="test", cascade={"persist"})
     */
    private $collection;

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

    ...

    public function addCollection( Breakagedemand $breakagedemand )
    {
        // Устанавливаем  для каждого добавляемого в коллекцию элемента  тест, к которому он относится
        $breakagedemand->setTest($this);

        // И только после этого добавляем его в коллекцию
        $this->collection[] = $breakagedemand;

        return $this;
    }

    ...
}


А сущность Breakagedemand так:
/**
 * Breakagedemand
 * @ORM\Table(name="breakagedemand")
 * @ORM\Entity
 */
class Breakagedemand {
    /**
     * @var integer
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue
     */
    private $id;


    // cascade={"persist"} тут не нужен, с этой стороны мы не будем редактировать сущность Breakage
    // Название сущности выбрано неверное, так как тут не будет никакого ID, а будет экземпляр сущности Breakage
    /**
     * @var integer
     * @ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\Breakage", inversedBy="breakagedemands")
     * @ORM\JoinColumn(name="posbreakage_id", referencedColumnName="id")
     */
    private $posbreakage;



    // cascade={"persist"} тут не нужен, с этой стороны вы тест добавлять никогда не будете
    // referencedColumnName="id" должен быть ID - так как мы связываем после по ИД с таблицей тестов

    /**
     * @ORM\ManyToOne(targetEntity="Acme\AppBundle\Entity\Test", inversedBy="collection")
     * @ORM\JoinColumn(name="test_id", referencedColumnName="id")
     */
    private $test;


    // $this->test не является коллекцией, не надо объявлять new ArrayCollection() в конструкторе
    public function __construct()
    {
    }

    // соответственно метод должен быть не addTest а setTest
    public function setTest(Test $test = null)
    {
        $this->test = $test;

        return $this;
    }

    public function getTest()
    {
        return $this->test;
    }

    public function setPosbreakage(Breakage $posbreakage )
    {
        $this->posbreakage = $posbreakage;
        return $this;
    }

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

}


В комментах я написал где вы накосячили.

На сколько я понял вашу задачу, у вас есть заранее составленный список возможных поломок. И к тесту надо просто прикрепить некоторые из них. В этом случае вам лучше совсем убрать сущность Breakagedemand и просто сделать связь многие ко многим Test и Breakage(это если в сущности Breakagedemand не хранятся дополнительные данные и если одна поломка не должна добавляться несколько раз):

/**
 * Test
 * @ORM\Table(name="test")
 * @ORM\Entity
 */
class Test {
    /**
     * @var integer
     * @ORM\Column(name="id", type="integer")
     * @ORM\Id
     * @ORM\GeneratedValue
     */
    private $id;

    /**
     * Список номеров, в котором должно выйти объявление
     * @var \Doctrine\Common\Collections\Collection
     *
     * @ORM\ManyToMany(targetEntity="Breakage")
     * @ORM\JoinTable(name="test2breakage",
     *   joinColumns={
     *     @ORM\JoinColumn(name="test_id", referencedColumnName="id")
     *   },
     *   inverseJoinColumns={
     *     @ORM\JoinColumn(name="breakage_id", referencedColumnName="id")
     *   }
     * )
     */
    private $breakages;

    public function __construct()
    {
        $this->breakages = new ArrayCollection();
    }
    public function addBreakage(Breakage $breakage)
    {

        $this->breakages[] = $breakage;

        return $this;
    }

    /**
     * Remove nomer
     *
     * @param Breakage $breakage
     */
    public function removeBreakage(Breakage $breakage)
    {
        $this->breakages->removeElement($breakage);
    }

    /**
     * Get nomer
     *
     * @return \Doctrine\Common\Collections\Collection
     */
    public function getBreakages()
    {
        return $this->breakages;
    }
}

/** 
 * @ORM\Entity
 * 
 **/
class Breakage
{
    // ...
}


И форма для работы с такой сущностью будет в 100 раз проще:
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        ...
        $builder->add('breakages', 'entity', array(
            'class' => 'NameSpace\MyBundle\Entity\Breakage',
            'property' => 'name',
            'multiple'  => true
        ));
    }


Неймспейсы я брал на обум, если что поправите.
Ответ написан
Комментировать
@e-hot Автор вопроса
Спасибо за ответ.

Про то, чтобы убрать ArrayCollection из Breakagedemand - я до этого тоже додумался уже после публикации темы :)

Ваш вариант формы понятен - он у меня был изначален, но... этого мне достаточно, когда редактируется только один параметр и можно задавать для списка multiple или чекбоксы.
У меня же сущность Test - это прототип сущности с 55 полями, среди которых 4 поля это коллекции форм, где в каждой форме помимо одного параметра в виде списка необходимо работать еще с двумя-тремя параметрами и здесь multiple уже не сработает, например:
набор параметров в форме внутри коллекции:
1. Тип поломки (выбираем из списка); 2. Кол-во поломок (указываем в поле text); 3. Стоимость (указываем в поле text).
Соответственно, когда на странице добавляем новую форму поломки в коллекцию, у нас создается список для выбора типа и пустые text для кол-ва и стоимости.
При этом данные из таких коллекций форм должны сохраняться в отдельные таблицы БД, где связь между этими таблицами с таблицей test осуществляется через id test. Это так - небольшое отступление.

С остальными предложениями и вариантами буду разбираться - спасибо.
Ответ написан
Ваш ответ на вопрос

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

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