Symfony2/Doctrine: How to re-save an entity with a OneToMany as a cascading new row
Asked Answered
G

3

18

Firstly, this question is similar to How to re-save the entity as another row in Doctrine 2

The difference is that I'm trying to save the data within an entity that has a OneToMany relationship. I'd like to re-save the entity as a new row in the parent entity (on the "one" side) and then as new rows in each subsequent child (on the "many" side).

I've used a pretty simple example of a Classroom having many Pupils to keep it simple.

So me might have ClassroomA with id=1 and it has 5 pupils (ids 1 through 5). I'd like to know how I could, within Doctrine2, take that Entity and re-save it to the database (after potential data changes) all with new IDs throughout and the original rows being untouched during the persist/flush.

Lets first define our Doctrine Entities.

The Classroom Entity:

namespace Acme\TestBundle\Entity;

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

/**
 * @ORM\Entity
 * @ORM\Table(name="classroom")
 */
class Classroom
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

   /**
     * @ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom")
     */
    protected $pupils;

    public function __construct()
    {
        $this->pupils = new ArrayCollection();
    }       
    // ========== GENERATED GETTER/SETTER FUNCTIONS BELOW ============

}

The Pupil Entity:

namespace Acme\TestBundle\Entity;

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

/**
 * @ORM\Entity
 * @ORM\Table(name="pupil")
 */
class Pupil
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

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

    /**
     * @ORM\ManyToOne(targetEntity="Classroom", inversedBy="pupils")
     * @ORM\JoinColumn(name="classroom_id", referencedColumnName="id")
     */
    protected $classroom;   

    // ========== GENERATED FUNCTIONS BELOW ============
}

And our generic Action function:

public function someAction(Request $request, $id)
{
    $em = $this->getDoctrine()->getEntityManager();

    $classroom = $em->find('AcmeTestBundle:Classroom', $id);

    $form = $this->createForm(new ClassroomType(), $classroom);

    if ('POST' === $request->getMethod()) {
        $form->bindRequest($request);

        if ($form->isValid()) {
            // Normally you would do the following:
            $em->persist($classroom);
            $em->flush();

            // But how do I create a new row with a new ID 
            // Including new rows for the Many side of the relationship

            // ... other code goes here.
        }
    }

    return $this->render('AcmeTestBundle:Default:index.html.twig');
}

I've tried using clone but that only saved the parent relationship (Classroom in our example) with a fresh ID, while the children data (Pupils) was updated against the original IDs.

Thanks in advance to any assistance.

Gerianne answered 31/1, 2012 at 23:26 Comment(0)
E
31

The thing with clone is...

When an object is cloned, PHP 5 will perform a shallow copy of all of the object's properties. Any properties that are references to other variables, will remain references.

If you are using Doctrine >= 2.0.2, you can implement your own custom __clone() method:

public function __clone() {
    // Get current collection
    $pupils = $this->getPupils();

    $this->pupils = new ArrayCollection();
    foreach ($pupils as $pupil) {
        $clonePupil = clone $pupil;
        $this->pupils->add($clonePupil);
        $clonePupil->setClassroom($this);
    }
}

NOTE: before Doctrine 2.0.2 you cannot implement a __clone() method in your entity as the generated proxy class implements its own __clone() which does not check for or call parent::__clone(). So you'll have to make a separate method for that like clonePupils() (in Classroom) instead and call that after you clone the entity. Either way, you can use the same code inside your __clone() or clonePupils() methods.

When you clone your parent class, this function will create a new collection full of child object clones as well.

$cloneClassroom = clone $classroom;
$cloneClassroom->clonePupils();

$em->persist($cloneClassroom);
$em->flush();

You'll probably want to cascade persist on your $pupils collection to make persisting easier, eg

/**
 * @ORM\OneToMany(targetEntity="Pupil", mappedBy="classroom", cascade={"persist"})
 */
protected $pupils;
Ewall answered 1/2, 2012 at 1:18 Comment(2)
That did it, thank you. I was hoping to avoid having to loop through in a function but this was the simplest solution in the end. I saw Doctrine's EntityManager class has a "copy" function described as "Creates a copy of the given entity. Can create a shallow or a deep copy." The only catch is that function has one line that throws an exception "Not implemented."Gerianne
FYI in current Doctrine2 you can implement your own __clone and it will get called.Hydrofoil
T
2

I did it like this and it works fine.

Inside cloned Entity we have magic __clone(). There we also don't forget our one-to-many.

/**
 * Clone element with values
 */
public function __clone(){
    // we gonna clone existing element
    if($this->id){
        // get values (one-to-many)
        /** @var \Doctrine\Common\Collections\Collection $values */
        $values = $this->getElementValues();
        // reset id
        $this->id = null;
        // reset values
        $this->elementValues = new \Doctrine\Common\Collections\ArrayCollection();
        // if we had values
        if(!$values->isEmpty()){
            foreach ($values as $value) {
                // clone it
                $clonedValue = clone $value;
                // add to collection
                $this->addElementValues($clonedValue);
            }
        }
    }
}
/** 
 * addElementValues 
 *
 * @param \YourBundle\Entity\ElementValue $elementValue
 * @return Element
*/
public function addElementValues(\YourBundle\Entity\ElementValue $elementValue)
{
    if (!$this->getElementValues()->contains($elementValue))
    {
        $this->elementValues[] = $elementValue;
        $elementValue->setElement($this);
    }

    return $this;
}

Somewhere just clone it:

// Returns \YourBundle\Entity\Element which we wants to clone
$clonedEntity = clone $this->getElement();
// Do this to say doctrine that we have new object
$this->em->persist($clonedEntity);
// flush it to base
$this->em->flush();
Tacket answered 30/9, 2014 at 11:12 Comment(0)
U
-4

I do this:

if ($form->isValid()) {
    foreach($classroom->getPupils() as $pupil) {
        $pupil->setClassroom($classroom);
    }
    $em->persist($classroom);
    $em->flush();
}
Unshapen answered 5/4, 2012 at 19:25 Comment(1)
This doesn't look like it will create new Pupil instances; it will share each Pupil between the two Classrooms.Cherianne

© 2022 - 2024 — McMap. All rights reserved.