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

Laravel Sortable Laravel Package

sofa/laravel-sortable

A Laravel package for making Eloquent models sortable, letting you reorder records and persist their positions with minimal setup. Handy for drag-and-drop lists and custom sort columns.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and immediately check constraints. composer require sofa/laravel-sortable. The only tagged release is from November 2016, so its illuminate/* constraint almost certainly pins Laravel 5.x. Before writing a line of code, confirm the solve succeeds against your framework version; if it doesn't, the realistic options are a fork with a loosened constraint or vendoring the (small) trait into app/Support/.
  2. Add the column in a migration. The trait works against a positional integer column (default sort_order). Make it a non-nullable unsignedInteger with a default and a composite index on any grouping columns plus the sort column:
    $table->unsignedInteger('sort_order')->default(0);
    $table->index(['category_id', 'sort_order']);
    
  3. Backfill existing rows. Never leave every row at 0 — the trait cannot infer intent from ties. Seed positions in insertion order (UPDATE items SET sort_order = id is fine for a first pass) and only then start moving things.
  4. Wire the trait into one model. use Sofa\Eloquent\Sortable\Sortable; (namespace depends on the release you pin — read the file you actually installed) and, if you don't want sort_order, override the column name on the model.
  5. First use case: an admin reorder list. Model::sorted()->get() to render the list, $model->moveToPosition($n) from a drag-and-drop handler to persist. That single loop — read sorted, move one row, re-render — is 90% of day-to-day usage.

Where to look first: the trait source itself. It's a few hundred lines, it is the documentation, and every behaviour (how new rows get a position, whether moves renumber or swap, how the column name is resolved) is visible there in under ten minutes.

Implementation Patterns

Ordered reads. Always read through the scope (Model::sorted()->get()) rather than orderBy('sort_order') scattered through controllers, so a future change to direction or tie-breaking is one edit. For paginated admin tables add ->orderBy('id') as a deterministic tiebreak.

Moves, not assignments. Treat sort_order as owned by the trait. Day-to-day operations should be moveToStart(), moveToEnd(), moveBefore($other), moveAfter($other), moveToPosition($n). Setting sort_order by hand is how you get duplicates.

Prefer relative moves in the UI. Drag-and-drop usually computes an absolute index, which is fragile when the visible list is filtered. If the endpoint can send "place after item #42", use moveAfter($neighbor) — it stays correct regardless of what the user was filtering on. Fall back to moveToPosition() only when the client truly sees the whole scope.

Wrap every move in a transaction. Moves shift neighbouring rows; a failure mid-shift leaves holes or duplicates. In the controller:

DB::transaction(fn () => $item->moveToPosition($request->integer('position')));

Bulk reorder endpoint. For "save all" style UIs, accept an ordered array of IDs and assign 1..n deterministically in a single transaction instead of issuing n individual moves. This is also the cheapest way to repair a list that has drifted.

Grouped scopes. Multi-list scenarios (items per category, comments per post, menus per location) need the trait restricted to the group. Check what the pinned version supports for scope columns; if it's thin, a small local helper that only queries and moves within where('category_id', ...) is safer than assuming the trait handles it.

Creation hooks. Confirm in the source whether creating auto-assigns the next position. If it does, note that Model::insert(), DB::table()->insert(), and raw SQL all bypass it — write your seeders and importers with create()/save() so they go through the model events.

Testing. After a move, refresh() the affected models and assert the full ordered ID list rather than a single row's sort_order. Property-style assertions ("the list is a permutation of 1..n with no gaps") catch drift that single-value checks miss.

Gotchas and Tips

  • The package is effectively abandoned (1 star, zero dependents, last release 2016). Budget for it not installing, or for the trait being the only file you keep. Because it's tiny, porting it into app/Support/Sortable.php under your own namespace removes an entire class of supply-chain and compatibility risk — and lets you add the features below.
  • Duplicates and gaps are a given, not an exception. Manual updates, imports, soft deletes, and concurrent drags all produce ties or holes. Ship an artisan command (items:renumber) that reassigns 1..n per scope and run it after imports. Don't build UI that assumes contiguous values.
  • Concurrency. Two admins dragging simultaneously will interleave reads and writes. lockForUpdate() on the scoped rows inside the transaction, or switch the endpoint to sending the full ordered ID list — the latter is far simpler and idempotent.
  • NULL and default 0 behaviour. MySQL sorts NULL first; every row at 0 collapses into ties resolved by the engine's whim. Keep the column non-nullable and backfilled.
  • Soft deletes. Neighbour lookups that exclude trashed rows will compute a position that doesn't match the underlying data if trashed rows are still counted. Decide explicitly whether trashed records occupy a slot and use withTrashed() consistently.
  • sort_order drifts from id. After enough moves the two are unrelated; never use id as a proxy for display order in queries, exports, or "next article" links.
  • Cache invalidation. Sorted lists are prime cache fodder. Any move must forget the list cache for that scope — put the forget call inside the same service method that performs the move, not in the controller.
  • Mass assignment and validation. If your admin form ever accepts a position directly, that's an attack surface (users reordering other users' rows). Validate that the target model belongs to the scope the actor is allowed to reorder, and keep sort_order out of $fillable.
  • Debugging. Model::sorted()->toSql() plus DB::listen() will show you exactly which UPDATEs a move emits — the count tells you whether you're on the renumber-all strategy (cheap to reason about, expensive at scale) or the shift-window strategy. If a move appears to do nothing, check that save() actually fired: isDirty() on a no-op move is common.
  • Extension points worth adding yourself: a Sortable interface for type-hinted service methods, a nextSortOrder($scope) helper, and a reorder(array $ids, $scope) service method. These three cover the gaps you'll hit in the first week of real use.
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.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package