Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Sortable Behavior Bundle Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. Install: composer require runroom/sortable-behavior-bundle.
  2. Register the bundle in config/bundles.php (Flex recipe may do this):
    Runroom\SortableBehaviorBundle\RunroomSortableBehaviorBundle::class => ['all' => true],
    
  3. Add the behavior to your entity — implement the interface and use the trait, and add a 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;
    }
    
  4. Generate and run a Doctrine migration to add the position column (start nullable — see Gotchas).
  5. First use case: an admin listing (EasyAdmin, Sonata, or a custom controller) with "move up"/"move down" actions. Wire the buttons to the bundle's move/sort service, call it with the entity and a direction (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.

Implementation Patterns

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.

Gotchas and Tips

  • Nullable-then-non-nullable. Make 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.
  • Avoid a unique constraint on 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.
  • Re-entrancy. The listener reacts to entity changes it also causes. Watch for recursion when your move service updates a batch — most implementations guard with a flag or inspect the Doctrine ChangeSet. If you see duplicate reorder queries, that's the cause.
  • Concurrency. Two admins moving rows at the same time can leave gaps or duplicates. Wrap moves in a transaction and, for busy back offices, consider pessimistic locking on the scope.
  • Performance at scale. A "shift everything between old and new position by one" strategy is O(n) per move. Fine for tens or hundreds of rows; painful for thousands. For big lists, prefer sparse positions (gaps of 100) and reindex periodically.
  • Deletes leave holes. The bundle rarely compacts positions on delete unless you tell it to. Either accept gaps (harmless for ordering) or add an postRemove handler to close them, scoped correctly.
  • Config quirks. The field name is usually resolved via the trait's 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.
  • Version compatibility. It's a small, community-maintained bundle (low stars, few dependents). Verify constraints against your Symfony and Doctrine ORM versions before upgrade; ORM 3 / recent Symfony majors may not yet be supported. Pin the version in composer.json.
  • Extension points. To customize behavior, decorate the listener/sort service in 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.
  • Debugging tip. If moves "do nothing", check that (a) the entity is managed and flushed, (b) your repository is actually ordering by position, and (c) the listener is registered — bin/console debug:event-dispatcher doctrine.event_listener (or the subscriber equivalent) will confirm.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo