runroom-packages/sortable-behavior-bundle
Runroom Sortable Behavior Bundle adds easy sortable behavior to Symfony and Doctrine entities. Use a position field and trait to make records reorderable, then manage ordering in your admin or custom UI. Great for categories, menus, and any list that needs drag-and-drop sorting.
The bundle layers a Sonata Admin UI (drag‑and‑drop + move action) on top of Doctrine Extensions' Sortable behaviour. It does not implement sorting itself — so step one is always getting Gedmo wired up correctly.
gedmo/doctrine-extensions and, almost always, stof/doctrine-extensions-bundle:
composer require runroom-packages/sortable-behavior-bundle gedmo/doctrine-extensions stof/doctrine-extensions-bundle
config/bundles.php:
Runroom\SortableBehaviorBundle\RunroomSortableBehaviorBundle::class => ['all' => true],
stof_doctrine_extensions (this is the step people forget, and the symptom is that position is always null):
stof_doctrine_extensions:
orm:
default:
sortable: true
position integer column and the Gedmo annotations:
/**
* @Gedmo\Sortable
*/
class Article
{
/** @Gedmo\SortablePosition */
private ?int $position = null;
}
Add @Gedmo\SortableGroup on any column that scopes the list (category, locale, parent, …).AbstractAdmin and add position to configureListFields(). Point the move route at the bundle's controller (or override it) and include the provided JS asset in sonata_admin.yaml.position on subsequent loads. If that works end‑to‑end, the plumbing is right; everything else is refinement.Look first at the bundle's Resources/ directory: the controller action, the Twig row template, and the JS file tell you exactly what the front end expects (route name, required data attributes, HTTP method). That is your integration contract.
Plain drag‑and‑drop list. The canonical setup is: position as a list field, the bundle's JS loaded globally for the admin, and a move route declared under the admin. Every row carries its entity id and the JS posts a direction/delta to the controller, which delegates to Gedmo's SortableListener and reorders.
Grouped lists. When items belong to a parent (e.g. articles per category, blocks per page), annotate the parent with @Gedmo\SortableGroup. Two things then matter:
getSubject() and the route parameters), so the listener can resolve positions within the group.Programmatic reordering. Fixtures, imports, and console commands should not poke at position directly. Use the Gedmo listener or repository helpers so the listener can shift siblings:
$sortableListener->setSortablePosition($entity, 3);
or set position and flush inside a transaction so a failure does not leave gaps/duplicates.
Front‑end ordering. Once positions are maintained, expose ordering in your own queries with ORDER BY position ASC (or a group filter + position). Because positions can have gaps after deletes, never assume position === 0..n-1; only use it for ordering.
Extending the admin UI. The bundle is meant to be subclassed, not patched. Typical customisations: add up/down buttons for accessibility alongside drag‑and‑drop, add a "move to top / bottom" action, or replace the JS with a SortableJS-based variant while reusing the controller action.
Multiple sortable admins in one app. Each admin gets its own route name and template block. If you use a custom admin controller, keep it extendsing the bundle's controller rather than duplicating the move logic — the listener call plus position recalculation is the part you don't want to rewrite.
Integrating with Sonata filters/pagination. Sorting is defined on the full table (or full group), not on the filtered result set. If your admins offer filters that change the visible ordering, either hide drag‑and‑drop when a filter is active, or explicitly document that reordering is global. With pagination, ensure the move endpoint receives the page offset/limit (or performs a global move) so dragging across page boundaries doesn't produce surprising jumps.
position staying null after persist is almost always a missing sortable: true in stof_doctrine_extensions. Check that first before touching the bundle.position must be nullable (or have a default), because Gedmo assigns it on prePersist. A NOT NULL column without a default will fail on insert.(group, position) break reorders. The listener shifts rows one at a time and can transiently violate a unique index unless the move runs inside a transaction with deferred constraints (or without the index). This is the most common production incident with sortable behaviour.security.yaml, the drag silently stops working. Watch the browser network tab before blaming the controller.isGranted logic — add the check explicitly, otherwise any authenticated user can reorder anyone's data.position + 1 to the next row will be wrong; always sort by position, id.stof enabled), and set explicit position values in fixtures so tests don't depend on insertion order.How can I help you explore Laravel packages today?