15

I have a form that allows me to save a record or duplicate it. The form saves the record as a $view entity, which happens to have multiple associated entities, e.g. $viewVersion that are managed by the form builder in a formType with nested entities (this probably is irrelevant).

If I make changes and submit the form to "duplicate", the code clones the $view object with a function on my entity that unsets the $view->id and other associations. This forces Doctrine to make a new record when it persists the record to the database. This works perfectly. Hurray!

BUT, the changes made to the record are ALSO persisted to the original entity that was cloned (and consequently saved to the database). So it is saving these changes to TWO database records. I happen to like this functionality, but I need to understand WHY it's doing it so it doesn't break later. Here are the relevant bits of code in summary:

// File: CmsBundle/Controller/AdminEditController.php

// Get the Entity Manager
$em = $this->getDoctrine()->getManager();

// Get the View based on the requested ID
// Is there some magic that happens here to make the entity manager track this $view entity?
$view = $em->getRepository("GutensiteCmsBundle:View\View")->find($request->query->get('id'));

// Various bits of code to do whatever I want before a save
// ...

if ($request->isMethod( 'POST' )) {
    $form->handleRequest($request);
    if( $form->isValid() ) {
        // Duplicate the view entity if the view button is pushed
        if(
            $form->has('duplicate') 
            && $form->get('duplicate')->isClicked()
        ) {
            $view = clone $view;
        }

        // Persist the cloned view
        $em->persist($view);
        $em->flush();
    }
}

The View entity has a special clone function that gets triggered on a clone, which resets the ids of the cloned versions:

// File: CmsBundle/Entity/View.php

public function __clone() {
    if($this->id) {
    $this->setId(null);
    $this->setLockVersion(1);
    $this->setPublished(null);

    // Clone associated entities and reassociate with THIS version (even though there is no id yet, there will be when it persists)
    // clone the current version (which also has a clone function like this)
    $version = clone $this->getVersion();
    // reset the viewid with a custom function
    $version->resetView();
    // Add this cloned verion to the version history
    $this->addVersion($version);
}

I've read a lot about cloning, and consistently I'm told that you don't need to detach the original $view from the entity manager. Besides, I've tried, and it didn't do any good. The changes to $view, which were submitted by the form and processed to $view prior to the cloning, are still saved to the original $view record id (e.g. 33), as well as to the new cloned record (e.g. 62). So two persists are happening, even though, only one persist is called on a single entity.

What is going on?

Update

I am told that if you load an entity with the entity manager, it is being tracked by the entity manager. So if you call flush() at any time, any changes will be persisted, even if you did not call persist($view) on the entity. So when I clone the entity, the entity manager is effectively managing 2 entities: the original and the clone.

I've tried detaching the view from the entity manager before the clone in two ways:

// detach method 1
$em->detach($view); 
$em->flush();

// detach method 2
$em->refresh($view); 
$em->flush();

// clone the view after detaching the first entity.
$view = clone $view;

But the entity manager still persists the changes to the original $view record.

I also tried the suggestion to add unset($this->_entityPersister, $this->_identifier); to my custom __clone() method. But that also did not detach the the original entity or the cloned version from the entity manager. The changes were saved to both the old record and the new record.

Nothing seems to make the entity manager ignore the original entity.

Useful References

Community
  • 1
  • 1
Chadwick Meyer
  • 7,041
  • 7
  • 44
  • 65
  • I think you're overriding symfony's built in `__clone()` method which prevents the behavior you describe. Try adding `unset($this->_entityPersister, $this->_identifier);` to your `__clone()` method. For more info see http://stackoverflow.com/a/9071208/3574819 – FuzzyTree Jul 12 '14 at 02:09
  • An entity that is hydrated has lots of methods that wrap the original entity which could potentially lead to these side effects, try FuzzyTree's solution or maybe call `parent::__clone();`. – Thomas Potaire Jul 12 '14 at 04:43
  • @FuzzyTree I had read that answer previously, and it seems like no one is quite sure how the magic of clone works :D (even that guy with 42,000 points). However, I just tried that suggestion, and unfortunately, it's still persisting changes to the original $view, as well as the clone. – Chadwick Meyer Jul 14 '14 at 17:30
  • @ThomasPotaire I also tried adding `parent::__clone();` to my version of clone, and it gives a symfony error: `Fatal error: Call to undefined method Gutensite\CmsBundle\Entity\Base::__clone() in /var/www/core/cms/src/Gutensite/CmsBundle/Entity/View/View.php on line 234` That's because I extend a base class entity. But if I add __clone to the base class and call the parent, I get the error: `Fatal error: Cannot access parent:: when current class scope has no parent in /var/www/core/cms/src/Gutensite/CmsBundle/Entity/Base.php on line 180`. – Chadwick Meyer Jul 14 '14 at 17:38
  • But after doctrine 2.0.2, doctrine supposedly extends my custom __clone() with it's code (http://stackoverflow.com/a/9089445/3334390). So I don't think I need to call `parent::__clone()` – Chadwick Meyer Jul 14 '14 at 17:40
  • Did you try moving the clone logic in your controller and removing the `__clone` method? I am pretty sure cloning a hydrated entity should reset the Doctrine wrapper ~ kind of hard to help out without access to your codebase – Thomas Potaire Jul 14 '14 at 20:10
  • "Moving the clone logic in your controller" to where? To another function? I need a custom clone method because it has associated entities that also need to be cloned and some fields that need to be changed, e.g. `$view->title = ." (copy)"`. The references I used in my question describe this method. I suppose I could do a native clone, and then call a $view->resetClone() function that did that. I have yet to find a solid "standard" recommendation for this... – Chadwick Meyer Jul 14 '14 at 20:58
  • Just to be clear, when I added `unset($this->_entityPersister, $this->_identifier);` to my custom `__clone()` method, it did not detach the the original entity or the cloned version manager. The changes were saved to both the old record and the new record. Any ideas? – Chadwick Meyer Jul 14 '14 at 21:05
  • BTW, I also converted the custom `__clone()` method to a `resetClone()` method, and then called the native `$view = clone $view` followed by `$view->resetClone()`. So that way it used the native clone method and my custom method just did the extra resets. But that didn't help either... No matter what I do, I can't get the entity manager to stop saving the original entity. #voodoo – Chadwick Meyer Jul 14 '14 at 21:59
  • If someone is looking for the answer I found this post. https://stackoverflow.com/a/32457976/699436 – gastoncs Oct 11 '17 at 19:41

1 Answers1

11

Persist is only needed when you attaching something to your Entity Manager . But in your case original "$view record id (e.g. 33)" already within it. So basically, what happens:

$view1 = new View();
$view1->text = '1';
$em->persist($view1);
$em->flush();

Now your have one record with text == '1' stored. Then:

$view1->text = 'one'; //important!

$view2 = new View();
$view2->text = 'two';

$view3 = new View();
$view3->text = 'three';

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

Call of flush() updates your $view1, inserts your $view2, and ignores your $view3 since last is not persisted. As a result you have two records 'one' and 'two'.

It is possible to call flush() for selected objects. So call of $em->flush($view2) will only insert $view2 and leave $view1 untouched.

In your simple example it will work.

But make sure $em->flush() won't happen any further.

Otherwise to be sure that your $view1 will stay unchanged try to $em->refresh($view1) it.

halfer
  • 19,824
  • 17
  • 99
  • 186
Dropaq
  • 121
  • 4
  • Just to note, I overwrite the variable `$view` with the clone. I don't have two different variables. If that makes a difference. – Chadwick Meyer Jul 14 '14 at 17:47
  • So you are saying that whenever an entity is created in my code, it is automatically managed by the entity manager? So even if I don't call `$em->persist($view)` when a `flush()` is called, any changes to that entity will be saved to the database? That's unexpected. What is the point of a persist() function then, if everything is automatically persisted. Are you certain? BTW, even when I called $em->detach($view) prior to cloning it, it still persisted the original. – Chadwick Meyer Jul 14 '14 at 17:48
  • No, it does not make any difference. Since entity is actually an object stored within Entity Manager, and your `$view` is only temporary link to it. Assuming `$views` is a collection of your `View\View` objects. `foreach ($views as $view) { $view->setText(null); }` will work as well, despite the fact you don't have a variable for them – Dropaq Jul 14 '14 at 18:11
  • About your second question: not exactly. When you create an entity in your code you should call `$em->persist()`. It's necessary to obtain new ID for an entity to be able to assign it as a relation to another entity, and it tells Entity Manager that this particular object should be stored in the DB and be managed by EM. So after that - EM assumes that object as an entity and always manage it. When you load it from DB by `$view = $em->getRepository("...")->find($id);` EM already knows that this object is an entity, and persist not required. And yes, I'm 100% sure about that. – Dropaq Jul 14 '14 at 18:22
  • I've added `$view3` to the example. – Dropaq Jul 14 '14 at 19:09
  • I do understand that you have to call `persist()` and `flush()` on a new entity, in order to save it to the db and get a record id. But in the case where the record already exists, it is fetched with entity manager, and then the entity is cloned, then the entity manager is managing 2 entities: the original and the clone. So when you call flush() it will persist both. So shouldn't a $em->detach() keep the first from being persisted? – Chadwick Meyer Jul 14 '14 at 21:04
  • I've tested doing a `$em->detach($view); $em->flush();` as well as `$em->refresh($view); $em->flush();` before doing the clone, and it still persists the changes to the original `$view` record. So I'm not sure why nothing works to tell the entity manager not to manage the entity and persist it. If you are certain those SHOULD work, then I will assume something else is going on. I just need to be sure what the magic of doctrine is actually doing behind the scenes, because this is unexpected... – Chadwick Meyer Jul 14 '14 at 21:17
  • Honestly `$em->detach($view);` should do the trick. And I don't know why it doesn't. Probably the problem somewhere else in your code. I've tried to implement similar code, and all works like a charm, **even without detaching**. So, could you please provide all code where you are modifying $view. And what changes exactly ported from new entity to original (only Lock Version and Published or something else?). Do you modify an original $view prior to clone it? If so why? Maybe flush() happens in some function without you knowing it and changes was stored in DB before clone()? – Dropaq Jul 15 '14 at 08:45
  • There is so much code it's hard to post. But I agree that detach should work. I'll try to track this down and post more code if I need more help. FYI, the reason changes happen to the entity is that it's an edit page, and if someone makes a change before they hit "duplicate" the changes are merged from the form post to the entity, before I can clone it. – Chadwick Meyer Jul 15 '14 at 17:15
  • Can you `$em->detach($view)` from the entity manager, after you have processed the form post via `$form->handleRequest($request)`? I don't know what voodoo the form is doing, but it has it's claws into the entity really deep. – Chadwick Meyer Jul 15 '14 at 17:21
  • 1
    Just word of warning: looks like `flush($specific_entity)` for specific entity doesn't always supposed to work as expected http://www.doctrine-project.org/jira/browse/DDC-2496 – Dimitry K Nov 23 '14 at 10:36