0

I've been trying to figure out how to get this to work for along time but without any luck. Due to a complex logic in an app I'm working on, I need to create an isolated clone of a entity collection without preserving what so ever relation to the database. Whatever changes I do on the cloned collection should not be tracked by Doctrine at all and should be treated as if it doesn't exist at all.

Here's an example code:

 /* 
  * @ORM\Entity()
  */
class Person
{
    /**
     * @var integer
     *
     * @ORM\Id
     * @ORM\Column(name="person_id", type="integer",nullable=false)
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @var ArrayCollection
     *
     * @ORM\OneToMany(targetEntity="Car", mappedBy="person", cascade={"persist"})
     */
    public $cars;
}



/**
 * @ORM\Entity()
 * @ORM\HasLifecycleCallbacks()
 */
class Car
{
    /**
     * @var integer
     *
     * @ORM\Id
     * @ORM\Column(name="car_id", type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    private $id;

    /*
     * @ORM\JoinColumn(name="person_id", referencedColumnName="person_id", nullable=true)
     * @ORM\ManyToOne(targetEntity="Person", inversedBy="cars", cascade={"persist"})
     */
    private $person;

}

I've already tried the following code in my controller to store the collection into the session but it still somehow stores the relationships:

 $tmp = clone $person;
 $this->get('session')->set('carCollection', $tmp->getCars());

 $tmpCars = clone $person->getCars();
 $tmpCollection = new ArrayCollection();
 foreach($tmpCars as $car) {
     $tmpCollection->add(clone $car);
 }

 $this->get('session')->set('carCollection', $tmpCollection);

 $tmpCars = clone $person->getCars();
 $tmpCollection = new ArrayCollection();
 foreach($tmpCars as $car) {
     $clone = clone $car;
     $entityManager->detach($car);
     $tmpCollection->add(clone $clone);
 }

 $this->get('session')->set('carCollection', $tmpCollection);

Apparently I'm doing something wrong here because I end up having more results in the Car collection when flushing the entity even though the collection itself has the correct number of records. I have a suspicion that somewhere in the chain Doctrine doesn't compute correctly what needs to be done.

Any ideas or directions on how to solve or debug this?

Follow-up question: When retrieving back the cloned collection from the session will it still be an isolated clone or Doctrine will try merge it back?

tftd
  • 16,203
  • 11
  • 62
  • 106
  • 1
    This might help http://stackoverflow.com/questions/14158111/deep-clone-doctrine-entity-with-related-entities – flec May 31 '15 at 08:18
  • Thank you for your comment! I've tried that already, but the problem persists. What I'm literally trying to do is have a copy of the collection in the session, modify it (in the session). When the user submits the actual form, the collection from the session goes through a business logic which decides what to go into the actual collection. Again - this business logic works fine because when I do `$person->getCars()->count()` right before persisting the `$person` it returns the correct number of entities with the correct data. – tftd May 31 '15 at 20:20
  • 1
    I've been storing entities in the session as well and I would not recommend it. Its very prone to error. The problem is that you have to merge the main entity using the `merge` method. After that you have to refresh the relations. That means that you have to create a temp-collection (as you did) get the id from the existing entities and add references with the `getReference` method. I would recommend storing the form data instead. – flec Jun 01 '15 at 06:09
  • Could you please give me an example? I believe you're talking about directly storing `$myForm->getData()` into the session and then use it directly from the session? Is that correct? – tftd Jun 01 '15 at 13:34
  • 1
    Yes I think that could be a good approach. Unfortunately I'm not familiar with symfony2 but I think that you could bind the stored form data to the form in another request. Then using the form to update you entities. – flec Jun 01 '15 at 14:14

2 Answers2

1

I'm writing this answer to give directions to anybody who might have similar issues. I couldn't find many topics or documentation in this manner which is why I decided to share my experience. I am no deep expert on Doctrine an how it internally works, so I won't go into big details of "how it works". I will rather focus on the end result.

Storing entities which have relations to other entities into a session is quite problematic. When you retrieve it from the session, Doctrine loses track of the relationships (OneToMany, ManyToOne, etc). This leads to some undesired effects:

  1. Doctrine wrongly decides to insert a new record of an existing entity.
  2. Doctrine might throw exceptions such as A new entity was found through the relationship 'Acme\MyBundle\Entity\Person#cars' that was not configured to cascade persist operations for entity: Opel. To solve this issue: Either explicitly call EntityManager#persist() on this unknown entity or configure cascade persist this association in the mapping for example @ManyToOne(..,cascade={"persist"}). and at least 2 other types of exceptions which might seem totally irrelevant at first.

Apparently when fetching a result from the database and it "as-is" in your session things get really messy, specially if the entity has relations to other entities (which was my case). Pay big attention if you have entity relationships - they might need to be "refreshed" if you start getting strange exceptions.

There are a couple of ways to overcome this issue. One of which is to use the data sent via the form (as @flec suggested) by using $myForm->getData(). This approach might work well for you, but unfortunately it was not the case with me (too complex to explain).

What I ended up doing was implementing the \Serializable in the entity. I also created a method called __toArray() which converted my entity into an array. What data you return in the __toArray() method is totally up to you and your business logic. The array data is stored into the session and you use it to re-create a fresh object with all necessary relations.

Hope this helps somebody.

tftd
  • 16,203
  • 11
  • 62
  • 106
0

I think hydrators/extractors would be the way to go for you.

They can extract the data from an entity and you can pass them to a newly created instance of that entity via the hydrator. The only thing you'll need to do in between is the unsetting of the relation properties. They should be fetchable via a metadata class via doctrine somehow.

func0der
  • 2,192
  • 1
  • 19
  • 29