Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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.

like image 598
dividebyzeroZA Avatar asked Jan 31 '12 23:01

dividebyzeroZA


2 Answers

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;
like image 163
Phil Avatar answered Oct 19 '22 00:10

Phil


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();
like image 36
derkien Avatar answered Oct 19 '22 00:10

derkien