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