Sonataadminbundle: What's the best way to manage many-to-many relations that has extra columns on middle table?

Created on 15 Mar 2013  路  12Comments  路  Source: sonata-project/SonataAdminBundle

I have 3 tables : A,AB,B. AB is the middle table of table A and B.

for some reason I need a extra column on the middle table AB,So I have to split the many-to-many relation to 2 one-to-many relations( A has many AB, B also has many AB ).

then I found out that It seems like not possible to manage the relation. Can I manage(link/unlink) B in the edit page of A ?

any one have any idea ?

Most helpful comment

For the Entities you have to use two one-to-many relations between A-AB and AB-B to implement a many-to-many relationship with extra columns (http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table.

First, create your AAdmin, ABAdmin and BAdmin Classes like you would, if those Classes wouldn't share any relation. After this, add to your sonata.admin.A service a Child sonata.admin.AB (Admin.yml or Admin.xml).

Than you can use the configureSideMenu() method in AAdmin to create a sidemenu and generate a link to ABAdmin.

It's important to add $parentAssociationMapping to your ABAdmin Class and adding some if-statements to improve the usability, if list and form is in a A-context.

If you know the mechanism, it's quite easy. Hope my example works for you..

#   Admin.yml
#   Add a Child in the Service of A
services:
    sonata.admin.A:
        class: Path\toBundle\Admin\AAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: YourGroup, label: "label.A" }
        arguments:
            - ~
            - Path\toBundle\Entity\A
            - 'SonataAdminBundle:CRUD'
        calls:
            - [ setTranslationDomain, [toBundle]]
            - [ addChild, [@sonata.admin.AB]]
class AAdmin extends Admin
{
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->with('General')
                ->add('name')
                ->add('data')
                /* No add('AB') here.. as you see in my screenshot, I added it inline before. */
            ->end()
         ;
    }
    // ...

    protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
    {
        if (!$childAdmin && !in_array($action, array('edit'))) {
            return;
        }
        $admin = $this->isChild() ? $this->getParent() : $this;
        $id = $admin->getRequest()->get('id');
        $menu->addChild(
            $this->trans('admin.sidemenu.link_view_A'),
            array('uri' => $admin->generateUrl('edit', array('id' => $id)))
        );
        $menu->addChild(
            $this->trans('admin.sidemenu.link_view_AB'),
            array('uri' => $admin->generateUrl('sonata.admin.AB.list', array('id' => $id)))
        );   
    } 
}
class ABAdmin extends Admin
{
    protected $parentAssociationMapping = 'A'; // This does the trick..

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('B')
            ->add('AB_Data')
        ;

        if (!$this->isChild()) 
            $formMapper->add('A'); // Just add the A dropdown, if you are NOT in A-context
    }
    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('AB_Data')
            ->add('B')
        ;
    }
    protected function configureListFields(ListMapper $listMapper)
    {
        if (!$this->isChild())
            $listMapper->addIdentifier('id')->addIdentifier('A');

        $listMapper
            ->addIdentifier('B')
            ->addIdentifier('AB_Data');
    }
}
?>

The relations in the AB Entity are defined as the following (I added a surrogate key Id)

class AB
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
    protected $id;

/**
* @ORM\ManyToOne(targetEntity="B", inversedBy="ABs")
* @ORM\JoinColumn(name="B_id", referencedColumnName="id", nullable=false)
*/
    protected $B;

/**
* @ORM\ManyToOne(targetEntity="A", inversedBy="As")
* @ORM\JoinColumn(name="A_id", referencedColumnName="id", nullable=false)
*/
    protected $A;
/**
* @ORM\Column(type="string", length=10, nullable=true)
*/
    protected $AB_Data;

All 12 comments

I ran in the same issue as you. In my case issue #1000 is related to this problem, too, because I used the primary keys of A and B as Compound key in AB (http://en.wikipedia.org/wiki/Compound_key)

After introducing a surrogate key AB.id I could workaround #1000 and can add and delete the Relation in the A-Form. As you can see in the screenshot attached, I can add a new Entry to AB (Factors is AB, Category is B and Regatta is A). But the Regatta/A Column should be associated to the Regatta/A-Form I'm currently using..

I'm new to SF and Sonata, but I'll try to find a workaround for. I'm pleased about every hint.
ishot-1

thanks @smlr , I'm happy that you figure it out , but Could you explain more about how you did it ?

If you can provide some example code that will be very helpful, thanks !

For the Entities you have to use two one-to-many relations between A-AB and AB-B to implement a many-to-many relationship with extra columns (http://docs.doctrine-project.org/en/2.1/reference/faq.html#how-can-i-add-columns-to-a-many-to-many-table.

First, create your AAdmin, ABAdmin and BAdmin Classes like you would, if those Classes wouldn't share any relation. After this, add to your sonata.admin.A service a Child sonata.admin.AB (Admin.yml or Admin.xml).

Than you can use the configureSideMenu() method in AAdmin to create a sidemenu and generate a link to ABAdmin.

It's important to add $parentAssociationMapping to your ABAdmin Class and adding some if-statements to improve the usability, if list and form is in a A-context.

If you know the mechanism, it's quite easy. Hope my example works for you..

#   Admin.yml
#   Add a Child in the Service of A
services:
    sonata.admin.A:
        class: Path\toBundle\Admin\AAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: YourGroup, label: "label.A" }
        arguments:
            - ~
            - Path\toBundle\Entity\A
            - 'SonataAdminBundle:CRUD'
        calls:
            - [ setTranslationDomain, [toBundle]]
            - [ addChild, [@sonata.admin.AB]]
class AAdmin extends Admin
{
    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->with('General')
                ->add('name')
                ->add('data')
                /* No add('AB') here.. as you see in my screenshot, I added it inline before. */
            ->end()
         ;
    }
    // ...

    protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null)
    {
        if (!$childAdmin && !in_array($action, array('edit'))) {
            return;
        }
        $admin = $this->isChild() ? $this->getParent() : $this;
        $id = $admin->getRequest()->get('id');
        $menu->addChild(
            $this->trans('admin.sidemenu.link_view_A'),
            array('uri' => $admin->generateUrl('edit', array('id' => $id)))
        );
        $menu->addChild(
            $this->trans('admin.sidemenu.link_view_AB'),
            array('uri' => $admin->generateUrl('sonata.admin.AB.list', array('id' => $id)))
        );   
    } 
}
class ABAdmin extends Admin
{
    protected $parentAssociationMapping = 'A'; // This does the trick..

    protected function configureFormFields(FormMapper $formMapper)
    {
        $formMapper
            ->add('B')
            ->add('AB_Data')
        ;

        if (!$this->isChild()) 
            $formMapper->add('A'); // Just add the A dropdown, if you are NOT in A-context
    }
    protected function configureDatagridFilters(DatagridMapper $datagridMapper)
    {
        $datagridMapper
            ->add('AB_Data')
            ->add('B')
        ;
    }
    protected function configureListFields(ListMapper $listMapper)
    {
        if (!$this->isChild())
            $listMapper->addIdentifier('id')->addIdentifier('A');

        $listMapper
            ->addIdentifier('B')
            ->addIdentifier('AB_Data');
    }
}
?>

The relations in the AB Entity are defined as the following (I added a surrogate key Id)

class AB
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
    protected $id;

/**
* @ORM\ManyToOne(targetEntity="B", inversedBy="ABs")
* @ORM\JoinColumn(name="B_id", referencedColumnName="id", nullable=false)
*/
    protected $B;

/**
* @ORM\ManyToOne(targetEntity="A", inversedBy="As")
* @ORM\JoinColumn(name="A_id", referencedColumnName="id", nullable=false)
*/
    protected $A;
/**
* @ORM\Column(type="string", length=10, nullable=true)
*/
    protected $AB_Data;

thanks @smlr, I will have a try.

Hello,
I'm trying to do the same, but I have a problem. Everything is working out except that I would like to add several AB entities from my A form just like you in your screenshot (if I understood well) but I still stuck.
And you wrote in your code : " /* No add('AB') here.. as you see in my screenshot, I added it inline before. */ "
What do you mean by "I added it inline before" ?

In my AB Admin I can't use 'sonata_type_collection' to add several AB entities at once...

In any case, thanks a lot @smlr without your example, I would be blocked yet, this is only a detail but I need it so if you have an issue : it would be great !

Finally, I fixed it by myself.
But when I try to add an entity A from A form with some AB entities it doesn't work.
It says : "[A_id, B_id, AB_data] : A_id is null"
Keep try'in

Finally fixed it too.

    public function preUpdate($object)
    {
        $this->prePersist($object);
    }

    public function prePersist($object)
    {
        foreach($object->getAB() as $AB)
            $AB->setA($object);
    }

@belette-ninja can You tell me where You put that code? IT looks like EntityAdmin in sonata Admin; and how You where able to use Your functions ->getAB using the $object?

You have to put it in your A admin file. (A the owner of the association and B the children).
Secondly, if you followed what is above. You do have a getter for the AB attribute in your A entity. Don't forget to generate your getters/setters with :

php app/console doctrine:generate:entities App/YourBundle/Entity/A
php app/console doctrine:generate:entities App/YourBundle/Entity/B
php app/console doctrine:generate:entities App/YourBundle/Entity/AB

Before I start anything more complex I'd like to confirm what this solution provides or what I am expecting:
I have classes A and B from A I want to add Meny B and from B I want to see Many A's that can be associated with it. This creates ManyToMany between A and B but I require some data (confirmation that A reached B) in table in between - so AB is created with custom fields.

Now I want to populate AB from A by selecting many B destinations and have AB filled with id B_1 and A_n and from B I can see what different A's are connected to it. I don't quite understand why Child Admin is needed (but I dont understand that concept entirely yet) for this that's why I'm asking.

I'll try to follow Your advice but just one more thing. If I put the code in Admin (where prePersist functions reside etc.) how is it possible to get data that is usually accessed by the controller?

I am new to Symfony and Sonata and still learning so please forgive my ignorance ;).

@ethernal you should look at the SonataMediaBundle, we have a Gallery, linked to GalleryHasMedia, linked to a Media. So you have a GalleryAdmin, a GalleryHasMediaAdmin and MediaAdmin.

From the the GalleryAdmin it is possible to add a GalleryHasMedia, one side of the many-to-many, and the GalleryHasMediaAdmin has a field to the MediaAdmin, the other side of the many-to-many. With this pattern you can edit extra information from the GalleryHasMedia

screen shot 2014-01-30 at 17 09 26

@rande Thanks, I had everything in place and well, after changing sonata types it started working as expected. It was mistake on my part as I was thinking more about using multiple=true option of sonata_type_model to add many instances of users. Now I just need to filter the list that appears when adding "join-table" elements.

Was this page helpful?
0 / 5 - 0 ratings