Symfony 4/ Sonata: Create a Nested Interface

We are going to see how to build an admin interface composed of several tables that have Many2Many relationships.

Let's revisit our example of a many/many interface available here
We have a zone table, which is made up of several elements of the departments table. On these departments, we have agencies.
To top it off, and to give meaning to this data chain, we add a zx_credential table, which represents salespeople.
Here is our data chain: Salespeople->Zones->Departments->Agencies.

data model workbench

For our project, we modeled the entire database via MysqWorkbench, and exported the schema to MySql.

All that's left is to export the entities (with getters and setters), and create the CRUD Sonata

php bin/console doctrine:mapping:import "App\Entity" annotation --path=src/Entity
php bin/console make:entity --regenerate App
php bin/console make:sonata:admin App/Entity/ZxZone
php bin/console make:sonata:admin App/Entity/ZxCredential
php bin/console make:sonata:admin App/Entity/Agences
php bin/console make:sonata:admin App/Entity/Departement
php bin/console cache:clear

For form's sake, we rename our table "zx_credential" to "Commercial Access".
In the service.yaml file, just modify the "Label" argument.

    admin.zx_credential:
        class: App\Admin\ZxCredentialAdmin
        arguments: [~, App\Entity\ZxCredential, App\Controller\ZxCredentialAdminController]
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: "Accès commerciaux" }
        public: true
Sélection_131

So far, nothing out of the ordinary. But what we want is to have an interface with the entire data chain linked together, so that when we edit a salesperson, we have the option to go directly into the configuration of their zone, then their departments, then their agencies.
For this, we will specify the table links with each other via a call to the "addChild" method in our CRUD service. And we will specify the children of each table.
For the process to work we must specify the child service, as well as the parent field used for the link.

In our ZxCredential->ZxZone link, we have the following field in our child entity (ZxZone):

Sélection_133

So this is the one that is used for the link, and that we will use in our configuration.

    admin.zx_credential:
        class: App\Admin\ZxCredentialAdmin
        arguments: [~, App\Entity\ZxCredential, App\Controller\ZxCredentialAdminController]
        calls:
            - [addChild, ["@admin.zx_zone", 'zxCredential']] 
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: "Accès commerciaux" }
        public: true

The complete configuration looks like this:

   admin.departement:
        class: App\Admin\DepartementAdmin
        arguments: [~, App\Entity\Departement, ~]
        calls:
            - [addChild, ["@admin.agences","departement"]] 
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: Departement }
        public: true
        
    admin.zx_zone:
        class: App\Admin\ZxZoneAdmin
        arguments: [~, App\Entity\ZxZone, App\Controller\ZxZoneAdminController]
        calls:
            - [addChild, ["@admin.departement", "zxZone"]] 
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: Zones }
        public: true

    admin.zx_credential:
        class: App\Admin\ZxCredentialAdmin
        arguments: [~, App\Entity\ZxCredential, App\Controller\ZxCredentialAdminController]
        calls:
            - [addChild, ["@admin.zx_zone", 'zxCredential']] 
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: "Accès commerciaux" }
        public: true

    admin.agences:
        class: App\Admin\AgencesAdmin
        arguments: [~, App\Entity\Agences, ~]
        tags:
            - { name: sonata.admin, manager_type: orm, group: admin, label: Agences }
        public: true

At this stage, all our tables are linked, but we will need to configure the menu to navigate between them.
For this, we will add the configureSideMenu method in our ZxCredentialAdmin sales admin.
Be careful, it's in the entry point of the interface that you have to configure the menu. So the entire Salespeople->Zones->Departments->Agencies chain will be done within Salespeople.

First step, we add the uses in all our admin interfaces, so that it's done.

<?php

declare(strict_types=1);

namespace App\Admin;

use Sonata\AdminBundle\Admin\AbstractAdmin;
use Sonata\AdminBundle\Datagrid\DatagridMapper;
use Sonata\AdminBundle\Datagrid\ListMapper;
use Sonata\AdminBundle\Form\FormMapper;
use Sonata\AdminBundle\Show\ShowMapper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;

/*gestion de nos interfaces imbriquées*/
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;

Next, we add our first link to zone management, from the Zx_Credential interface.

   protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null): void
    {
        if (!$childAdmin && !\in_array($action, ['edit'], true)) {
            return;
        }
         
        $admin = $this->isChild() ? $this->getParent() : $this;
        $id = $admin->getRequest()->get('id');
        $label=$this->hasSubject() && null !== $this->getSubject()->getLabel() ? $this->getSubject()->getLabel():null;
        

    
        $menu->addChild(
            'Configuration de l\'accès commercial : '.$label,
            $admin->generateMenuUrl('edit', ['id' => $id])
             
            );
    
        $child=$menu->addChild( 'Listes des zones',
            [
                'uri' => $admin->generateUrl('admin.zx_zone.list', ['id' => $id])
            ]);
        
        
        
    }
    

Basically, what you need to remember, and what is not clear in the documentation, is the route to configure. We need the name of the route, then the id.
For the id, it's simple, for a first level, it's always:

$id = $admin->getRequest()->get('id');

And for the route, it's always the same thing, it's the name of the service followed by the type of view. So here admin.zx_zone.list
This gives us:

$child=$menu->addChild( 'Listes des zones',
            [
                'uri' => $admin->generateUrl('admin.zx_zone.list', ['id' => $id])
            ]);
commercial_zone-1

At this stage, we have our first nested interface. For the following ones, we will repeat the configuration.
What follows is not explained in the documentation because it is obvious logic. But you may search for a long time if you do not understand the operation of the nesting of interfaces.
It is important to understand that everything is done from the first interface. The inclusion of the menu buttons and the construction of the routes.
The first, then the first+second, then the first+second+third.

The interface must always be built within our first interface, for each step. For this, we need to retrieve the id at each stage, and build the route.
And to retrieve the keys, the trick is to scrutinize the routes generated by symfony, via the debug:router command

php bin/console debug:router
---------------------------------------------------------- ---------- -------- ------ ------------------------------------------------------------------------------------------------------------- 
  Name                                                       Method     Scheme   Host   Path                                                                                                         
 ---------------------------------------------------------- ---------- -------- ------ ------------------------------------------------------------------------------------------------------------- 
  _preview_error                                             ANY        ANY      ANY    /_error/{code}.{_format}                                                                                     
  _wdt                                                       ANY        ANY      ANY    /_wdt/{token}                                                                                                
  _profiler_home                                             ANY        ANY      ANY    /_profiler/                                                                                                  
  _profiler_search                                           ANY        ANY      ANY    /_profiler/search                                                                                            
  _profiler_search_bar                                       ANY        ANY      ANY    /_profiler/search_bar                                                                                        
  _profiler_phpinfo                                          ANY        ANY      ANY    /_profiler/phpinfo                                                                                           
  _profiler_search_results                                   ANY        ANY      ANY    /_profiler/{token}/search/results                                                                            
  _profiler_open_file                                        ANY        ANY      ANY    /_profiler/open                                                                                              
  _profiler                                                  ANY        ANY      ANY    /_profiler/{token}                                                                                           
  _profiler_router                                           ANY        ANY      ANY    /_profiler/{token}/router                                                                                    
  _profiler_exception                                        ANY        ANY      ANY    /_profiler/{token}/exception                                                                                 
  _profiler_exception_css                                    ANY        ANY      ANY    /_profiler/{token}/exception.css   
  admin_app_departement_list                                 ANY        ANY      ANY    /admin/app/departement/list                                                                                  
  admin_app_departement_create                               ANY        ANY      ANY    /admin/app/departement/create                                                                                
  admin_app_departement_batch                                ANY        ANY      ANY    /admin/app/departement/batch                                                                                 
  admin_app_departement_edit                                 ANY        ANY      ANY    /admin/app/departement/{id}/edit                                                                             
  admin_app_departement_delete                               ANY        ANY      ANY    /admin/app/departement/{id}/delete                                                                           
  admin_app_departement_show                                 ANY        ANY      ANY    /admin/app/departement/{id}/show                                                                             
  admin_app_departement_export                               ANY        ANY      ANY    /admin/app/departement/export                                                                                
  admin_app_departement_agences_list                         ANY        ANY      ANY    /admin/app/departement/{id}/agences/list                                                                     
  admin_app_departement_agences_create                       ANY        ANY      ANY    /admin/app/departement/{id}/agences/create                                                                   
  admin_app_departement_agences_batch                        ANY        ANY      ANY    /admin/app/departement/{id}/agences/batch                                                                    
  admin_app_departement_agences_edit                         ANY        ANY      ANY    /admin/app/departement/{id}/agences/{childId}/edit                                                           
  admin_app_departement_agences_delete                       ANY        ANY      ANY    /admin/app/departement/{id}/agences/{childId}/delete                                                         
  admin_app_departement_agences_show                         ANY        ANY      ANY    /admin/app/departement/{id}/agences/{childId}/show                                                           
  admin_app_departement_agences_export                       ANY        ANY      ANY    /admin/app/departement/{id}/agences/export                                                                   
  admin_app_zxcredential_list                                ANY        ANY      ANY    /admin/app/zxcredential/list                                                                                 
  admin_app_zxcredential_create                              ANY        ANY      ANY    /admin/app/zxcredential/create                                                                               
  admin_app_zxcredential_batch                               ANY        ANY      ANY    /admin/app/zxcredential/batch                                                                                
  admin_app_zxcredential_edit                                ANY        ANY      ANY    /admin/app/zxcredential/{id}/edit                                                                            
  admin_app_zxcredential_delete                              ANY        ANY      ANY    /admin/app/zxcredential/{id}/delete                                                                          
  admin_app_zxcredential_show                                ANY        ANY      ANY    /admin/app/zxcredential/{id}/show                                                                            
  admin_app_zxcredential_export                              ANY        ANY      ANY    /admin/app/zxcredential/export                                                                               
  admin_app_zxcredential_zxzone_list                         ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/list                                                                     
  admin_app_zxcredential_zxzone_create                       ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/create                                                                   
  admin_app_zxcredential_zxzone_batch                        ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/batch                                                                    
  admin_app_zxcredential_zxzone_edit                         ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/edit                                                           
  admin_app_zxcredential_zxzone_delete                       ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/delete                                                         
  admin_app_zxcredential_zxzone_show                         ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/show                                                           
  admin_app_zxcredential_zxzone_export                       ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/export                                                                   
  admin_app_zxcredential_zxzone_departement_list             ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/list                                               
  admin_app_zxcredential_zxzone_departement_create           ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/create                                             
  admin_app_zxcredential_zxzone_departement_batch            ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/batch                                              
  admin_app_zxcredential_zxzone_departement_edit             ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/edit                                
  admin_app_zxcredential_zxzone_departement_delete           ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/delete                              
  admin_app_zxcredential_zxzone_departement_show             ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/show                                
  admin_app_zxcredential_zxzone_departement_export           ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/export                                             
  admin_app_zxcredential_zxzone_departement_agences_list     ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/list                        
  admin_app_zxcredential_zxzone_departement_agences_create   ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/create                      
  admin_app_zxcredential_zxzone_departement_agences_batch    ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/batch                       
  admin_app_zxcredential_zxzone_departement_agences_edit     ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/{childChildChildId}/edit    
  admin_app_zxcredential_zxzone_departement_agences_delete   ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/{childChildChildId}/delete  
  admin_app_zxcredential_zxzone_departement_agences_show     ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/{childChildChildId}/show    
  admin_app_zxcredential_zxzone_departement_agences_export   ANY        ANY      ANY    /admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/export                      
  admin_app_agences_list                                     ANY        ANY      ANY    /admin/app/agences/list                                                                                      
  admin_app_agences_create                                   ANY        ANY      ANY    /admin/app/agences/create                                                                                    
  admin_app_agences_batch                                    ANY        ANY      ANY    /admin/app/agences/batch                                                                                     
  admin_app_agences_edit                                     ANY        ANY      ANY    /admin/app/agences/{id}/edit                                                                                 
  admin_app_agences_delete                                   ANY        ANY      ANY    /admin/app/agences/{id}/delete                                                                               
  admin_app_agences_show                                     ANY        ANY      ANY    /admin/app/agences/{id}/show    

And our particular attention will focus on our route admin_app_zxcredential_zxzone_departement_agences_list which contains our entire data chain.
It is composed of our tables with the name of the ids that we will retrieve to build the routes of our interfaces.
/admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/list
zxcredential: id
zxzone: childId
departement: childChildId

So by testing the presence of these variables in our incoming URLs, we will be able to define the depth of our interface and construct the menus accordingly.

For the routes it's simple, for each stage, we add the current route separated by a pipe |:
zxcredential: admin.zx_zone.list
zxzone: admin.zx_zone|admin.departement.list
departement: admin.zx_zone|admin.departement|admin.agences.list

This gives us:

   protected function configureSideMenu(MenuItemInterface $menu, $action, AdminInterface $childAdmin = null): void
    {
        if (!$childAdmin && !\in_array($action, ['edit'], true)) {
            return;
        }
         
        $admin = $this->isChild() ? $this->getParent() : $this;
        $id = $admin->getRequest()->get('id');
        $label=$this->hasSubject() && null !== $this->getSubject()->getLabel() ? $this->getSubject()->getLabel():null;
        

    
        $menu->addChild(
            'Configuration de l\'accès commercial : '.$label,
            $admin->generateMenuUrl('edit', ['id' => $id])
             
            );
    
        $child=$menu->addChild( 'Listes des zones',
            [
                'uri' => $admin->generateUrl('admin.zx_zone.list', [
                    'id' => $id
                    
                ])
            ]);
        
        if(!empty($admin->getRequest()->get('childId'))){
            $child=$menu->addChild( 'Listes des departements',
                [
                    'uri' => $admin->generateUrl('admin.zx_zone|admin.departement.list', [
                        'id' => $id, 
                        'childId' => $admin->getRequest()->get('childId')
                        
                    ])
                ]);
        }
        if(!empty($admin->getRequest()->get('childChildId'))){
            $child=$menu->addChild( 'Listes des agences',
                [
                    'uri' => $admin->generateUrl('admin.zx_zone|admin.departement|admin.agences.list', [
                        'id' => $id, 
                        'childId' => $admin->getRequest()->get('childId'),
                        'childChildId' => $admin->getRequest()->get('childChildId')
                    ])
                ]);
        }

    }

Our interface is now complete:

commercial_to_agence