Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Doctrine2 ManyToMany record not created after deep clone

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.

database diagram segment - many to many relation

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.

Example contents of the offer_price table before cloning

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

Intended contents of the 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 definitions - simplified 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;                                                       
    }                                                                       


    ...                                                                     
}                                                                           

Offer

/**                                                                         
 * @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;                         

            ...                                                             

        }                                                                   
    }                                                                       

    ...                                                                     
}                                                                           

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.

like image 343
helvete Avatar asked Oct 30 '22 00:10

helvete


1 Answers

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

like image 74
Fabien Salles Avatar answered Nov 13 '22 16:11

Fabien Salles