Doctrine2 ManyToMany record not created after deep cloning

I ran into the following problem. An application should be able to clone a Season object with all the objects associated with it. I inspired this great question - and everything works as it should, but there is a problem with the ManyToMany attitude on the way.

Please take a look at the attached image, which depicts a small part of the database diagram showing the section that I'm having problems with.

database chart segment - many-to-many relationship

The state I want to achieve is to have a clone of the Price object associated with an existing Offer object. It is clear that I cannot and should not clone the Offer object, the new cloned instance of the Price object must be bound to the same instance to which the Price wizard instance is bound.

Sample contents of offer_price table before cloning

  offer_id | price_id ----------+---------- 47 | 77 

Estimated contents of offer_price table after cloning

  offer_id | price_id ----------+---------- 47 | 77 47 | 79 

... assuming Price ID 77 is the master record, and Price ID 79 is the newly cloned instance associated with the same Offer record.

Entity definitions - simplified as much as possible

Price

 /** * @Entity */ class Price { ... /** * @var \Doctrine\Common\Collections\Collection of Offer * @ManyToMany(targetEntity="Offer", mappedBy="prices", cascade={"persist"}) * * @get * @set * @add * @remove * @contains */ private $offers; /** * Class construct * * @return void */ public function __construct() { parent::__construct(); $this->offers = new ArrayCollection(); } /** * Clone entity * * @return void */ public function __clone() { if ($this->getId()) { $this->setId(null); $this->offers = new ArrayCollection(); } } /** * Add and offer into offers collection * * @param Offer $offer * @return self */ public function addOffer(Offer $offer) { $this->offers->add($offer); return $this; } ... } 

Sentence

 /** * @Entity */ class Offer { ... /** * @var \Doctrine\Common\Collections\Collection of Price * @ManyToMany(targetEntity="Price", inversedBy="offers", cascade={"persist"}) * * @get * @set * @add * @remove * @contains */ private $prices; /** * Class construct * * @return void */ public function __construct() { parent::__construct(); $this->prices = new ArrayCollection(); } ... } 

Season

 /** * @Entity */ class Season { ... /** * @var \Doctrine\Common\Collections\Collection of Price * @OneToMany(targetEntity="Price", mappedBy="season", cascade={"persist", "remove"}) * * @get * @set * @add * @remove * @contains */ private $prices; /** * Class construct * * @return void */ public function __construct() { parent::__construct(); $this->prices = new ArrayCollection(); } /** * Clone entity * * @return void */ public function __clone() { if ($this->getId()) { $this->setId(null); ... $priceClonedCollection = new ArrayCollection(); foreach ($this->prices as $price) { $priceClone = clone $price; $priceClone->setSeason($this); foreach ($price->getOffers() as $offer) { $priceClone->addOffer($offer); } $priceClonedCollection->add($priceClone); } $this->prices = $priceClonedCollection; ... } } ... } 

I consist in that I have all the objects in the relation that I need, but only until the whole set is saved. After cleaning all the objects, preserving the parent ( Season ), everyone else will get a cascade, like them, except for the ManyToMany binding ManyToMany , where new entries are not added.

The solution that I used in the application is rather dirty. After clearing all the saved objects, I simply iterate over the Offer records associated with the Price instance (since they are correctly connected to each other) and save all identifiers, which are then manually inserted into the database. This solution is obviously not ideal and rather fragile.

 ... /** * Return an array consisting of mappings that have to be inserted manually * * @param Season $season * @return array */ public function getCloneBindingHack(Season $clone) { foreach ($clone->getPrices() as $price) { foreach ($price->getOffers() as $offer) { $bindingHack[] = [ 'offer_id' => $offer->getId(), 'price_id' => $price->getId(), ]; } } return $bindingHack ?? []; } ... 

Therefore, I am interested in how to persist in such a relationship. I assume there is an elegant solution that I just skipped, as these operations are quite common in real-world scenarios. But perhaps Doctrine2 cannot do it this way: “You have to do it yourself, since Doctrine cannot help you” might also be the right answer (which would make ORM completely useless IMHO).

Just to add objects on both sides, ManyToMany relationships are created and saved, everything works as it should, so I assume that the ManyToMany relationship binding table is annotated correctly.

PHP version 7.0.22
Doctrine2 ORM version 2.4.8

Note. I read this this question , but it does not address the same issue.

+5
source share
1 answer

For your problem: this is because you do not have a reference to the offer object for your price clone (required aside mappedBy ). Try something like this:

 /** * Clone entity * * @return void */ public function __clone() { if ($this->getId()) { $this->setId(null); ... $priceClonedCollection = new ArrayCollection(); foreach ($this->prices as $price) { $priceClone = clone $price; $priceClone->setSeason($this); foreach ($price->getOffers() as $offer) { $offer->addPrice($priceClone); $priceClone->addOffer($offer); } $priceClonedCollection->add($priceClone); } $this->prices = $priceClonedCollection; ... } } 

For your ugly part: this question has already been asked , and the answer suggested this package

+2
source

Source: https://habr.com/ru/post/1272821/


All Articles