Symfony 4/Sonata: Crear una Interfaz Anidada

Vamos a ver cómo construir una interfaz de administración compuesta por varias tablas que tienen relaciones de Muchos a Muchos.

Revisemos nuestro ejemplo de una interfaz muchos a muchos disponible aquí
Tenemos una tabla de zonas, que está compuesta por varios elementos de la tabla de departamentos. En estos departamentos, tenemos agencias.
Para rematar, y darle sentido a esta cadena de datos, añadimos una tabla zx_credential, que representa a los vendedores.
Aquí está nuestra cadena de datos: Vendedores->Zonas->Departamentos->Agencias.

data model workbench

Para nuestro proyecto, hemos modelado toda la base de datos a través de MysqWorkbench y exportado el esquema a MySql.

Lo único que queda es exportar las entidades (con sus getters y setters) y crear el CRUD de 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

Por razones de forma, renombramos nuestra tabla "zx_credential" a "Acceso Comercial".
En el archivo service.yaml, basta con modificar el argumento "Label".

    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

Hasta ahora, nada fuera de lo común. Pero lo que queremos es tener una interfaz con toda la cadena de datos enlazada, de modo que cuando editemos a un vendedor, tengamos la opción de ir directamente a la configuración de su zona, luego a sus departamentos y luego a sus agencias.
Para esto, especificaremos los enlaces de las tablas entre sí mediante una llamada al método "addChild" en nuestro servicio CRUD. Y especificaremos los hijos de cada tabla.
Para que el proceso funcione debemos especificar el servicio hijo, así como el campo padre utilizado para el enlace.

En nuestro enlace ZxCredential->ZxZone, tenemos el siguiente campo en nuestra entidad hija (ZxZone):

Sélection_133

Así que este es el que se usa para el enlace, y que utilizaremos en nuestra configuración.

    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

La configuración completa se ve así:

   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

A esta altura, todas nuestras tablas están enlazadas, pero necesitaremos configurar el menú para navegar entre ellas.
Para esto, agregaremos el método configureSideMenu en nuestro admin de ventas ZxCredentialAdmin.
Tengan cuidado, es en el punto de entrada de la interfaz donde hay que configurar el menú. Por lo tanto, toda la cadena Vendedores->Zonas->Departamentos->Agencias se hará dentro de Vendedores.

Primer paso, añadimos los uses en todas nuestras interfaces de administración, para que esté hecho.

<?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;

Luego, añadimos nuestro primer enlace a la gestión de zonas, desde la interfaz Zx_Credential.

   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])
            ]);
        
        
        
    }
    

En esencia, lo que necesitas recordar, y lo que no está claro en la documentación, es la ruta a configurar. Necesitamos el nombre de la ruta, luego el id.
Para el id, es simple, para un primer nivel, siempre es:

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

Y para la ruta, siempre es lo mismo, es el nombre del servicio seguido por el tipo de vista. Así que aquí admin.zx_zone.list
Esto nos da:

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

En esta etapa, tenemos nuestra primera interfaz anidada. Para las siguientes, repetiremos la configuración.
Lo que sigue no se explica en la documentación porque es una lógica obvia. Pero podrías buscar durante mucho tiempo si no entiendes el funcionamiento de la anidación de interfaces.
Es importante entender que todo se hace desde la primera interfaz. La inclusión de los botones del menú y la construcción de las rutas.
La primera, luego la primera+segunda, luego la primera+segunda+tercera.

La interfaz siempre debe construirse dentro de nuestra primera interfaz, en cada paso. Para esto, necesitamos recuperar el id en cada etapa y construir la ruta.
Y para recuperar las claves, el truco es escudriñar las rutas generadas por symfony, a través del comando debug:router

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    

Y nuestra especial atención se centrará en nuestra ruta admin_app_zxcredential_zxzone_departement_agences_list que contiene toda nuestra cadena de datos.
Se compone de nuestras tablas con el nombre de los ids que recuperaremos para construir las rutas de nuestras interfaces.
/admin/app/zxcredential/{id}/zxzone/{childId}/departement/{childChildId}/agences/list
zxcredential: id
zxzone: childId
departement: childChildId

Así que probando la presencia de estas variables en nuestras URLs entrantes, podremos definir la profundidad de nuestra interfaz y construir los menús en consecuencia.

Para las rutas es simple, para cada etapa, añadimos la ruta actual separada por una tubería |:
zxcredential: admin.zx_zone.list
zxzone: admin.zx_zone|admin.departement.list
departement: admin.zx_zone|admin.departement|admin.agences.list

Esto nos da:

   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')
                    ])
                ]);
        }

    }

Nuestra interfaz ahora está completa:

commercial_to_agence