I have encountered the following problem. The application needs to be able to clone a Season
entity with all of its related entities. I have inspired in this great question - and everything works as it should, but there is a problem with ManyToMany
relation on the way.
Please take a look at the attached image depicting a small part of the database diagram showing the section I am having problems with.
The state I want to achieve is to have a clone of a Price
entity bound to an existing Offer
entity. To put it clear - I cannot and must not clone the Offer
entity, the new cloned instance of the Price
entity has to be bound to the same instance the master Price
entity instance is bound to.
offer_price
table before cloning offer_id | price_id
----------+----------
47 | 77
offer_price
table after cloning offer_id | price_id
----------+----------
47 | 77
47 | 79
... assuming the Price
of ID 77 is the master record and the Price
of ID 79 is the newly cloned instance bound to the same Offer
record.
/**
* @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;
}
...
}
/**
* @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();
}
...
}
/**
* @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;
...
}
}
...
}
The state I am in is that I have all objects in a relation I need, but only until the whole set is persisted. After flushing all the objects by persisting the parent one (Season
), all the others get cascade persisted as they should except the ManyToMany
binding table, where no new records are being added.
The solution I employed in the application so far is quite dirty. After flushing all persisted objects I just iterate over the Offer
records bound to the Price
instance (since they are correctly bound to each other) and store all ids which are then being manually inserted into the database. This solution is obviously not ideal and quite 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 relations like this. I presume there is an elegant solution I am just missig as these operations are quite common in real world scenarios. But perhaps Doctrine2 is not able to do this so "You have to do that yourself as Doctrine cannot help you" can also be a valid answer (which would render the ORM quite useless IMHO).
Just to add - in case objects on both sides of the ManyToMany
relation are being newly created and persisted, everything works as it should so I presume the binding table of the ManyToMany
relation is annotated correctly.
PHP version 7.0.22
Doctrine2 ORM version 2.4.8
Note: I have read this this question, but it does not address the same issue.
For your problem : it is because you don't have link your offer object to your price clone (it is required in the mappedBy
side). 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 an answer has proposed this package
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With