E
E
e-hot2015-07-01 12:44:42
symfony
e-hot, 2015-07-01 12:44:42

When editing a collection of entities, new entities are not saved - why?

The essence of the problem:
there is a Test entity:

<?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 *************************************/
    ...
}

Object with specific id (code from controller):
$em = $this->getDoctrine()->getManager();
        $repo_test = $em->getRepository('Acme\AppBundle\Entity\Test');
        $test = $repo_test->findOneBy( array( 'breakagedemands' => $id ) );
       ...

forms the corresponding data into the form of the TestType collection:
...

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

    ...
}

The collection in this form is formed by the embedded BreakagedemandType form:
...
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' => 'Поломка: '
                                                    ) 
                    );
    }

    ...
}

The data in the BreakagedemandType form comes from the Breakagedemand entity:
/**
 * 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 *************************************/
}

And this Breakagedemand entity is accordingly connected with the Breakage entity (the essence of the Breakage entity is not important in the essence of the question). As a result, I get this form:
c86c7c5da40940de88158e82ed10a318.png
By sections of the manual:
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...
added js code with jquery to twig, as well as the recommended code in the controller (marked as "code to remove associations between a collection and an entity"):
...
    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(),
                                                                                ) 
                            );
    }

In the resulting form, as a result, everything is very coolly edited (types of breakdowns change) with saving in the database and also deleted - all this is also reflected in the database. But I can not overcome the problem of adding a new object to the collection and, accordingly, to the database. Adding process itself:
e492f1f216134b64a2e864f108ee356b.png
and error:
Found entity of type Doctrine\Common\Collections\ArrayCollection on association Acme\AppBundle\Entity\Breakagedemand#test, but expecting Acme\AppBundle\Entity\
Test .
Please look at the code with a fresh look, otherwise I most likely don’t notice any thread of bugs in the code, most likely in the link mapping. When adding an object to the collection, I check the state of existing entities through $em->getUnitOfWork()->getEntityState( $_item ) - says that (this is natural) all existing objects are MANAGED, and the added one is NEW.
Thank you in advance.

Answer the question

In order to leave comments, you need to log in

2 answer(s)
J
jaxel, 2015-07-02
@jaxel

Oh, well, you messed up here. How does it actually work?))
If done by your method, then the Test entity should look something like this:

/**
 * 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;
    }

    ...
}

And the essence of Breakagedemand is like this:
/**
 * 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;
    }

}

In the comments I wrote where you messed up.
As far as I understand your task, you have a pre-compiled list of possible breakdowns. And you just need to attach some of them to the test. In this case, you'd be better off removing the Breakagedemand entity entirely and simply making a many-to-many relationship Test and Breakage (this is if the Breakagedemand entity doesn't store additional data and if one break shouldn't be added multiple times):
/**
 * 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
{
    // ...
}

And the form for working with such an entity will be 100 times simpler:
public function buildForm(FormBuilderInterface $builder, array $options)
    {
        ...
        $builder->add('breakages', 'entity', array(
            'class' => 'NameSpace\MyBundle\Entity\Breakage',
            'property' => 'name',
            'multiple'  => true
        ));
    }

Namespaces I took for granted, correct me if anything.

E
e-hot, 2015-07-02
@e-hot

Thanks for the answer.
About removing the ArrayCollection from Breakagedemand - I also thought of this after the topic was published :)
Your version of the form is understandable - I had it originally, but ... this is enough for me when only one parameter is edited and you can set multiple for the list or checkboxes.
For me, the Test entity is a prototype of an entity with 55 fields, among which 4 fields are collections of forms, where in each form, in addition to one parameter in the form of a list, you need to work with two or three more parameters and here multiple will no longer work, for example:
a set of parameters in the form inside the collection:
1. Breakdown type (choose from the list); 2. Number of breakdowns (specify in the text field); 3. Cost (indicate in the text field).
Accordingly, when we add a new breakdown form to the collection on the page, we create a list for choosing the type and empty text for the quantity and cost.
In this case, the data from such collections of forms must be stored in separate database tables, where the connection between these tables with the test table is carried out through the id test. This is so - a small digression.
I will look into other suggestions and options - thanks.

Didn't find what you were looking for?

Ask your question

Ask a Question

731 491 924 answers to any question