In short, we want to create a CRUD interface, 1N, with
which, when we are editing an item, we add a panel to manage all
the child items.
Here we have a wtype table, with a wconf
table that contains a series of records linked to a wtype item.
Just like for the implementation example of sortable with
drag'n'drop ( available here ) we are going to use the
following
components:pixassociates/sortable-behavior-bundle
and stof/doctrine-extensions-bundle
You will
therefore have to have previously created an entity that contains
a 1N relationship with a second entity.
The manipulation
consists only in injecting the second entity in the service
declaration call of the first with the argument “addChild” and the
reference to the parent service.

When we want to refer to a service, we use the chain used for the declaration (which is free), and we add “@” to indicate that it is a reference.
#config/services.yaml
services:
admin.wconf:
class: App\Admin\WconfAdmin
tags:
- { name: sonata.admin, manager_type: orm, label: "Configuration des types" }
public: true
arguments: [~, App\Entity\Wconf, 'PixSortableBehaviorBundle:SortableAdmin']
calls:
- [setPositionService, ["@pix_sortable_behavior.position"]]
admin.wtype:
class: App\Admin\WtypeAdmin
arguments: [~, App\Entity\Wtype, ~]
calls:
- [addChild, ["@admin.wconf"]]
tags:
- { name: sonata.admin, manager_type: orm, label: "Types de contrats" }
public: true
In the 2 admin controllers, you will need to add references to the following libraries:
use Knp\Menu\ItemInterface as MenuItemInterface;
use Sonata\AdminBundle\Admin\AdminInterface;
use Sonata\AdminBundle\Route\RouteCollection;
use Pix\SortableBehaviorBundle\Services\PositionORMHandler as PositionHandler;
Then in the Parent interface, we add the buttons.
For the url of
the child interface, we have to give it the reference of the
service so that it creates the routes. Since our service is
“admin.wconf”, the reference for the list will therefore be
'admin.wconf.list'
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 du contrat '.$label,
$admin->generateMenuUrl('edit', ['id' => $id])
);
$menu->addChild( 'Configuration des interfaces '.$label,
[
'uri' => $admin->generateUrl('admin.wconf.list', ['id' => $id])
]);
}
Now that our CRUD child interface is restricted to the scope of our
parent selection, and since we want to manage the order by
drag'n'drop, we will remove the possibility to sort the
list.
It simply involves adding the argument sortable=false
(default true), when defining the list fields.
protected function configureListFields(ListMapper $listMapper)
{
$listMapper->add('_action', null, array(
'actions' => array(
'move' => array(
'template' => '@PixSortableBehavior/Default/_sort_drag_drop.html.twig',
'enable_top_bottom_buttons' => false, //optional
),
),
))
;
$listMapper->addIdentifier('etape', null, ['label' => 'Etape','sortable'=>false]);
$listMapper->addIdentifier('position', null, ['label' => 'position','sortable'=>false]);
//$listMapper->addIdentifier('id', null, ['label' => 'id','sortable'=>false]);
$listMapper->addIdentifier('label', null, ['label' => 'Label','sortable'=>false]);
$listMapper->addIdentifier('synchro_field', null, ['label' => 'Destination du champs','sortable'=>false]);
$listMapper->add('actif', null, ['editable' => true,'sortable'=>false]);
}
We also want to preserve the default order, so that the user always
finds the same order with each reload.
There are two ways.
The first consists in overloading the default configuration:
public function __construct( $code, $class, $baseControllerName ) {
parent::__construct( $code, $class, $baseControllerName );
}
protected $datagridValues = array(
'_page' => 1,
'_sort_by' => 'position',
'_sort_order' => 'ASC',
);
The second method allows for finer sorting, for example on 2 fields by overriding the query used for the list.
public function createQuery($context = 'list')
{
$proxyQuery = parent::createQuery('list');
$proxyQuery->addOrderBy($proxyQuery->getRootAlias().'.etape', 'ASC');
$proxyQuery->addOrderBy($proxyQuery->getRootAlias().'.position', 'ASC');
return $proxyQuery;
}
And there we realize that if we deactivate the order of the
columns, our order configured in our query no longer works.
So,
what we are going to do is override the template for this list
only, and remove the code that manages the column headers, but
keep everything else.
The list is found in vendor/sonata-project/admin-bundle/src/Resources/views/CRUD/base_list.html.twig and our concerned block is “table_header”

We are going to add our template in the declaration of our service.
For that we must take the reference of the template, described
on this page: https://symfony.com/doc/master/bundles/SonataAdminBundle/reference/templates.html#global-templates
In
our case it is

And we will just add a call of type setTemplate with our configuration
admin.wconf:
class: App\Admin\WconfAdmin
tags:
- { name: sonata.admin, manager_type: orm, label: "Configuration des types" }
public: true
arguments: [~, App\Entity\Wconf, 'PixSortableBehaviorBundle:SortableAdmin']
calls:
- [setPositionService, ["@pix_sortable_behavior.position"]]
- [ setTemplate, [list, "Admin/wconf-list.html.twig"]]
We put: “Admin/wconf-list.html.twig”
This means that the system
will go look for the file
/templates/Admin/wconf-list.html.twig
This file must extend the
initial view of the list, and then redefine the block that manages
the header only. We really want to keep everything else.
So, we
start our file by giving the reference of the master template, and
then we redefine our block
#/templates/Admin/wconf-list.html.twig
{% extends '@SonataAdmin/CRUD/base_list.html.twig' %}
{% block table_header %}
<thead>
<tr class="sonata-ba-list-field-header">
{% for field_description in admin.list.elements %}
{% if admin.hasRoute('batch') and field_description.getOption('code') == '_batch' and batchactions|length > 0 %}
<th class="sonata-ba-list-field-header sonata-ba-list-field-header-batch">
<input type="checkbox" id="list_batch_checkbox">
</th>
{% elseif field_description.getOption('code') == '_select' %}
<th class="sonata-ba-list-field-header sonata-ba-list-field-header-select"></th>
{% elseif field_description.name == '_action' and app.request.isXmlHttpRequest %}
{# Action buttons disabled in ajax view! #}
{% elseif field_description.getOption('ajax_hidden') == true and app.request.isXmlHttpRequest %}
{# Disable fields with 'ajax_hidden' option set to true #}
{% else %}
{% set sortable = false %}
{% apply spaceless %}
<th class="sonata-ba-list-field-header-{{ field_description.type }}{% if sortable %} sonata-ba-list-field-header-order-{{ sort_by|lower }} {{ sort_active_class }}{% endif %}{% if field_description.options.header_class is defined %} {{ field_description.options.header_class }}{% endif %}"{% if field_description.options.header_style is defined %} style="{{ field_description.options.header_style }}"{% endif %}>
{% if field_description.getOption('label_icon') %}
<i class="sonata-ba-list-field-header-label-icon {{ field_description.getOption('label_icon') }}" aria-hidden="true"></i>
{% endif %}
{{ field_description.label|trans({}, field_description.translationDomain) }}
</th>
{% endapply %}
{% endif %}
{% endfor %}
</tr>
</thead>
{% endblock %}
Then we have our header without the links to sort the
content.
We make sure that the user is not going to manage the
order by drag'n'drop with a poorly configured sort and call us
every 5 minutes to tell us that it doesn't work.