runroom/sortable-behavior-bundle
Runroom Sortable Behavior Bundle adds configurable sortable behavior to Doctrine entities in Symfony apps. It manages position fields and collection ordering, with helpers for building drag-and-drop or manual sort controls in admin interfaces.
This is a Symfony/Doctrine bundle, not a Laravel package — if you're on Eloquent you'll need a different solution (e.g. spatie/eloquent-sortable). For Symfony apps:
composer require runroom/sortable-behavior-bundle.config/bundles.php (Flex recipe may do this):
Runroom\SortableBehaviorBundle\RunroomSortableBehaviorBundle::class => ['all' => true],
position column:
use Runroom\SortableBehaviorBundle\Behaviors\SortableInterface;
use Runroom\SortableBehaviorBundle\Behaviors\SortableTrait;
class Post implements SortableInterface
{
use SortableTrait;
/** @ORM\Column(type="integer", nullable=true) */
private $position;
}
position column (start nullable — see Gotchas).up/down), flush, and redirect back to the index sorted by position.Where to look first: the README's "Usage" section for the exact trait/service namespaces on your tagged version, then Behaviors/SortableTrait.php (to see what the trait actually provides) and the Doctrine listener that manages position assignment.
Default position on create. The listener should assign MAX(position) + 1 on prePersist when position is null. Rely on this so new rows land at the end without custom controller code.
Move-in-admin pattern. The canonical flow is: index action orders by position; each row exposes moveUp/moveDown actions that POST to a controller endpoint; the endpoint calls the bundle's sort service, flushes, and redirects (or returns JSON for a drag-and-drop UI). Keep the sorting logic in the service — don't hand-roll swaps in the controller.
Scoped sorting. If you need ordering within a parent (e.g. posts per category), check whether your version supports a grouping/scope concept before building. If not, the common workaround is a composite ordering (category_id, position) plus a custom repository method that recomputes positions only within the scope — the bundle's listener is generally global unless you override it.
Drag-and-drop endpoint. Accept { id, newPosition } (or { id, direction }), load the entity, call the service, flush inside a transaction, and return the re-ordered id list. The front-end should re-render from the server response rather than optimistically reordering, because a dropped row shifts everyone else.
Backfilling existing data. Write a one-off console command that iterates rows ordered by your natural key (e.g. created_at) within each scope and assigns sequential position values. Run it once after deploying the migration, before exposing move buttons.
Ordering queries. Always add ->orderBy('e.position', 'ASC') (plus scope) to listing repositories. It's tempting to sort in PHP, but you'll lose the benefit on paginated lists.
Testing. Use a real database (SQLite in-memory is fine) in functional tests and actually flush() — the listener logic is driven by Doctrine lifecycle events, so mocked EntityManagers won't exercise it.
position nullable in the first migration, backfill, then alter to NOT NULL in a second migration. Trying to add a non-nullable column to a populated table will fail.position (naively). Reordering typically rewrites several rows in a single flush; a plain UNIQUE(position) will blow up mid-transaction. If you need uniqueness, use a deferred constraint or drop it and rely on the service.ChangeSet. If you see duplicate reorder queries, that's the cause.postRemove handler to close them, scoped correctly.getPositionField(); if you rename the column, update that method and the Doctrine mapping. The bundle config (if present under runroom_sortable_behavior:) tends to be minimal — don't expect rich YAML options for scoping or strategy.composer.json.services.yaml rather than editing vendor code, and extend the trait in your own base entity to override getPositionField() or add scope helpers. The interface is the contract — code against SortableInterface in your own services so you can swap the implementation.position, and (c) the listener is registered — bin/console debug:event-dispatcher doctrine.event_listener (or the subscriber equivalent) will confirm.How can I help you explore Laravel packages today?