Persistir ManyToMany en los dos lados

Vamos a ver el modo de que las entidades relacionadas persistan, ManyToMany, desde los 2 sitios (owner y inversed)

Para persistir la entidad relacionada desde la entidad owner, no hay problema, todo funciona perfectamente, el problema viene cuando queremos persistir desde la entidad inversa (inversed).

Suponemos que tenemos 2 entidades, EntityOne.php (owner) y EntityTwo.php (inversed), y hay una relación ManyToMany entre ellas, y necesitamos que desde una Admin Class de SonataAdminBundle, podamos modificar la relación de los items de una y otra entidad.

Persistir ManyToMany

Lo primero es declarar las relaciones y las funciones propias de cada clase.

Entidad owner, EntityOne.php

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * EntityOne
 *
 * @ORM\Table()
 */
class EntityOne
{
    
    //...
    
    /**
     * @ORM\ManyToMany(targetEntity="EntityTwo", inversedBy="entityOne")
     * @ORM\JoinTable(name="entityone_entitytwo")
     */
    private $entityTwo;

    //...

    /**
     * Add entityTwo
     *
     * @param \AppBundle\Entity\EntityTwo $entityTwo
     * @return EntityOne
     */
    public function addEntityTwo(\AppBundle\Entity\EntityTwo $entityTwo)
    {
        $this->entityTwo[] = $entityTwo;
        return $this;
    }

    /**
     * Remove entityTwo
     *
     * @param \AppBundle\Entity\EntityTwo $entityTwo
     */
    public function removeEntityTwo(\AppBundle\Entity\EntityTwo $entityTwo)
    {
        $this->entityTwo->removeElement($entityTwo);
    }

    /**
     * Get entityTwo
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getEntityTwo()
    {
        return $this->entityTwo;
    }

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->entityTwo= new \Doctrine\Common\Collections\ArrayCollection();
    }

}

Y ahora la entidad inversed EntityTwo.php:

<?php

namespace AppBundle\Entity;

use Doctrine\ORM\Mapping as ORM;

/**
 * EntityTwo
 *
 * @ORM\Table()
 */
class EntityTwo
{
    
    //...    
    
    /**
     * @ORM\ManyToMany(targetEntity="EntityOne", mappedBy="entityTwo")
     */
    private $entityOne;
    
    //...   

    /**
     * Add entityOne
     *
     * @param \AppBundle\Entity\EntityOne $entityOne
     * @return EntityTwo
     */
    public function addEntityOne(\AppBundle\Entity\EntityOne $entityOne)
    {
        $this->entityOne[] = $entityOne;
        $entityOne->addEntityTwo($this);
    }

    /**
     * Remove entityOne
     *
     * @param \AppBundle\Entity\EntityOne $entityOne
     */
    public function removeEntityOne(\AppBundle\Entity\EntityOne $entityOne)
    {
        $this->entityOne->removeElement($entityOne);
        $entityOne->removeEntityTwo($this); 
    }

    /**
     * Get entityOne
     *
     * @return \Doctrine\Common\Collections\Collection 
     */
    public function getEntityOne()
    {
        return $this->entityOne;
    }

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->entityOne= new \Doctrine\Common\Collections\ArrayCollection();
    }

}

Importantes, son las líneas 33 y 44, se utilizan las clases de la entidad owner, para persistir estos datos.

la Admin Class del owner, nada que destacar, EntityOneAdmin.php

<?php

namespace AppBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\AdminBundle\Route\RouteCollection;

/**
 * Description of EntityOneAdmin
 *
 * @author Juanjo García
 */
class EntityOneAdmin extends Admin {

    protected function configureRoutes(RouteCollection $collection) {
        //...
    }

    // Fields to be shown on create/edit forms
    protected function configureFormFields(FormMapper $formMapper) {
        $formMapper
                ->add('entityTwo')
        ;
    }

    // Fields to be shown on filter forms
    protected function configureDatagridFilters(DatagridMapper $datagridMapper) {
        //...
    }

    // Fields to be shown on lists
    protected function configureListFields(ListMapper $listMapper) {
        //...
    }

    /**
     * @param ShowMapper $showMapper
     */
    protected function configureShowFields(ShowMapper $showMapper) {
        //...
    }
}

También es muy importante la configuración, en la Admin Class, forzando a que ejecute las clases propias de la entidad inversed y no de la owner, EntityTwoAdmin.php

<?php

namespace AppBundle\Admin;

use Sonata\AdminBundle\Admin\Admin;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Sonata\AdminBundle\Route\RouteCollection;

/**
 * Description of EntityTwoAdmin
 *
 * @author Juanjo García
 */
class EntityTwoAdmin extends Admin {

    protected function configureRoutes(RouteCollection $collection) {
        //...
    }

    // Fields to be shown on create/edit forms
    protected function configureFormFields(FormMapper $formMapper) {
        $formMapper
                ->add('entityOne', 'sonata_type_model', array('by_reference' => false, 'multiple' => true))
        ;
    }

    // Fields to be shown on filter forms
    protected function configureDatagridFilters(DatagridMapper $datagridMapper) {
        //...
    }

    // Fields to be shown on lists
    protected function configureListFields(ListMapper $listMapper) {
        //...
    }

    /**
     * @param ShowMapper $showMapper
     */
    protected function configureShowFields(ShowMapper $showMapper) {
        //...
    }
}

Esto es todo, ya podemos persistir las relaciones desde las dos Admin Class.

Referencias