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.
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

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):

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

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:
