mysam/sortable-behavior-bundle
A Symfony bundle that adds sortable behavior to Doctrine entities, making it easy to order records with position fields and reorder them in admin lists and other collections.
composer require mysam/sortable-behavior-bundle and register Pix\SortableBehaviorBundle\PixSortableBehaviorBundle in config/bundles.php (or AppKernel).position integer column to the entity you want to sort (default 0). Optionally use the bundled SortableTrait and implement SortableInterface so getPosition()/setPosition() come for free.Pix\SortableBehaviorBundle\Controller\SortableAdminController (this bundle is a Sonata Admin listing enhancer — it assumes Sonata and Doctrine).move route (/admin/foo/move/{id}/{position}) pointing at moveAction, or reuse the bundled template which wires drag-and-drop and up/down/top/bottom buttons.FAQ, Category, or Block listing where an editor reorders rows manually rather than editing a raw position number. Start with one admin and one entity, verify the move route flips positions, then replicate.Where to look first:
Controller/SortableAdminController.php — all move/reorder logic lives here.Model/SortableTrait.php + Model/SortableInterface.php — what your entity must implement.Resources/views/ — the list template that renders drag handles and buttons.DependencyInjection/Configuration.php — sortable_groups and other config keys.Tests/ (if present) — most reliable guide to the actual method signatures.Basic admin. Add a fake action column that renders the move widget:
protected function configureListFields(ListMapper $listMapper)
{
$listMapper
->addIdentifier('id')
->add('name')
->add('_action', null, [
'actions' => [
'move' => [
'template' => '@PixSortableBehavior/Default/_sort_drag_drop.html.twig',
'enable_top_bottom_buttons' => true,
],
],
]);
}
Group-scoped sorting. When rows belong to a parent (category, page, tenant), configure the group field so position is scoped per group rather than global. Override the group getter in your admin if the config key isn't enough — most real deployments end up doing that.
Subclass, don't fork. Override getPositionFieldName() if you're not using position, and override the move handler if you need custom redirects or flash messages. The controller methods are small and meant to be extended.
Drag-and-drop. The bundled template expects jQuery UI sortable plus the Sonata list layout. Ensure the assets are loaded via sonata_admin.assets and that your list template is the one the bundle ships; swapping in a custom list.html.twig usually means also copying the drag JS.
Programmatic reordering. setPosition($n); flush(); alone is not enough — you'll collide with siblings. Either funnel changes through the bundle's position handler, or in a transaction: shift affected rows, set the target, then reindex the whole group. Fixtures and tests need this just as much as user actions.
Integration tip. Treat the bundle as the UI layer only. Keep position maintained on prePersist/preUpdate (see gotchas) so imports, API writes, and fixtures don't leave the column in a broken state.
position = 0. Nothing sets the next index automatically in every path. Add a prePersist listener that sets position = MAX(position) + 1 within the group, or every new entity piles on the same slot.moveAction should redirect to the list/referer. If the UI looks unchanged, check the response redirect and the Sonata pager's sort/order — the list can be re-sorted client-side and mask your change.position in configureListFields alone. Pair it with the pager's default sort, or Sonata's datagrid may show rows out of order regardless of the column values.SELECT id, position FROM foo ORDER BY position — holes and duplicates account for most "it moved the wrong row" reports.How can I help you explore Laravel packages today?