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.
What the package actually is. sofa/laravel-sortable resolves to jarektkaczyk/eloquent-sortable, a trait-based ordering helper for Eloquent models. It maintains an integer position column per model (optionally scoped to a parent/group column) and exposes moveToStart, moveToEnd, moveBefore, moveAfter, moveUp, moveDown, and moveToPosition, wired into Eloquent's creating/saving/deleting events to keep the sequence contiguous. The problem domain is real and common: ordered lists in admin UIs, drag-and-drop galleries, ordered workflow steps, ranked records.
Architecture fit. The trait approach fits Laravel's idioms well — it is drop-in, non-invasive, and requires only a migration plus a use Sortable statement. Conceptually it is a clean fit for any Eloquent-backed ordered list. In practice, the package is a 2016-era artifact: the last release predates Laravel 5.4, and the repository signals (1 star, 0 dependents, composite score 0.005) indicate effectively zero ecosystem validation. The architectural pattern is sound; the specific implementation is not a viable dependency in its current state.
Integration feasibility — the blocking issues.
illuminate/support constraints around 5.2.*/5.3.* and a PHP floor of ^5.5 or ^7.0. On Laravel 9/10/11 with PHP 8.2+, composer require will fail on version resolution before any code runs. Adopting this means forking and rewriting composer.json at minimum.each(), dynamic property deprecations, implicit nullable parameter deprecations, and — most relevant here — signature drift in overridden Eloquent methods and event callbacks.upsert, insert, update on the query builder, saveQuietly, chunked mass updates, some MassPrunable flows) will silently corrupt ordering. There is no guardrail or integrity check in the package.sofa/laravel-sortable) does not match the repository (jarektkaczyk/eloquent-sortable). Before any consideration, confirm the Composer package name maps to the intended upstream, check the composer.json name/authors fields, and confirm the MIT license text is present. A near-zero-star package with a name/repo mismatch is a typosquat-shaped risk, however small the blast radius.moveBefore calls in the same scope can interleave reads and produce duplicate or gapped positions. Correctness requires SELECT ... FOR UPDATE inside a transaction, plus a unique constraint on (scope_id, position) — which MySQL cannot defer, so you need a two-phase swap through negative or offset positions. The 2016 implementation almost certainly does not do this rigorously.Key questions for the requester. Which models and roughly how many rows per ordering scope? Is the reorder operation user-facing and concurrent, or admin-only and serialized? Do you need tree/nesting semantics or just flat lists? Can you accept a fork maintained in-house, or is a maintained public dependency a hard requirement? What is the cost of a corrupted order (cosmetic vs. workflow-breaking)?
Verdict. Do not adopt as a dependency. The ordering pattern is worth keeping; this implementation is not. Either use a maintained equivalent (spatie/eloquent-sortable is the mature, actively maintained option in this space) or vendor a trimmed implementation internally — the trait is a few hundred lines and you will end up owning it either way.
Stack fit. The package targets Laravel 5.x / PHP 5.x–7.x. Against a modern stack (Laravel 9–11, PHP 8.1+) it does not fit without modification. Against a legacy Laravel 5.x application still in service, it would install and function, but you would be layering an unmaintained dependency onto an already-legacy platform — compounding, not reducing, upgrade debt.
Recommended path, in preference order.
spatie/eloquent-sortable covers the same trait-based integer-position model with active maintenance, PHP 8 support, current Laravel constraints, and a real user base. Migration cost is low: same position column concept, similar method names (moveOrderUp, moveOrderDown, moveBefore, moveAfter), so the diff is largely a trait swap plus rename of call sites.max(position)+1 on create, shift a range on move, compress on delete, all inside a transaction with row locks. You gain control over bulk-operation handling and integrity checks that no package provides.composer.json constraints, add PHP 8 compatibility fixes, add a test suite, and register the fork as a vcs/path repository.Migration path for a fork.
1.0.0-internal.1), strip the stale sofa/* package name to avoid confusion with the public package.composer.json: php: ^8.1, illuminate/support: ^9.0|^10.0|^11.0.position as unsignedInteger, plus a composite index on (scope_id, position) — or a unique constraint if you can guarantee serialized reorders.ROW_NUMBER() OVER (PARTITION BY scope_id ORDER BY <current_sort>)), and only then enable the trait writes.Compatibility and sequencing.
composer require in a throwaway branch of the target app. If it resolves, you have a legacy app and a different conversation. If it fails — the expected outcome — you have the evidence needed to justify options 1 or 2 to stakeholders. This is a half-day task and should gate everything else.max(position) equals count() per scope. Run it in staging for a sprint before trusting the feature in production.upsert, raw update, and saveQuietly break ordering. Put it in the model's docblock and in review checklists.API-shape caution. The move methods return booleans and fire no domain events of their own. If reordering needs to trigger cache invalidation, search reindexing, or audit logging, you will be wrapping every call site — budget for that wrapper as part of the integration, not as an afterthought.
Maintenance. This is the dominant cost. Zero dependents and no release since November 2016 means no upstream security patches, no PHP minor-version fixes, no Laravel compatibility work, and no issue triage. Any fork becomes a permanently owned internal asset with a named maintainer, a compatibility test matrix against each Laravel upgrade, and a place in your dependency-review process. The actual code volume is small — which caps the effort — but the ownership is indefinite.
Support. No vendor, no community, no documentation beyond the repository README and the author's blog link. Failure diagnosis falls entirely to your team. For a package whose failure mode is "silently wrong data," that is a meaningful escalation path gap: nobody upstream will tell you whether a behavior is a bug or intended.
Scaling characteristics. Integer-position ordering is O(n) per move in the worst case: moving an item to the start of a 10,000-row scope rewrites 10,000 rows. That is a single UPDATE ... SET position = position + 1 WHERE scope = ? AND position < ? — set-based and tolerable at low thousands, painful in the tens of thousands, and lock-heavy under concurrency because the shift touches a wide row range. Two mitigations, both out of scope for this package:
Read paths are fine: ORDER BY position with a composite index is a clean index scan. The scaling risk is entirely on the write side and entirely predictable — size the decision to your largest expected scope.
Failure modes to instrument.
GROUP BY scope_id, position HAVING COUNT(*) > 1 check.position == index.upsert, chunked updates, imports, and seeders bypass model events; positions collide or go null. The list appears sorted in dev and scrambles in production after the first bulk job.WHERE scope_id IS NULL behaves differently from WHERE scope_id = ? in index usage and in unique constraints. Test the null-scope case explicitly; it is the classic blind spot in this pattern.Ramp-up. Low. The surface area is a handful of trait methods and one column; a mid-level developer is productive in an hour. The real learning is not the API but the invariants: which Eloquent operations bypass the trait, why every reorder must be transactional, and why the position column needs a composite index. Budget for those in onboarding docs rather than assuming the package teaches them.
Total cost of ownership. Low code surface, high ownership tail. The engineering decision is not "is this package good" — it is not, on the evidence — but "do we want an unmaintained dependency whose failure mode is silent data disorder, or do we want to own fifty lines of transaction-wrapped shifting logic." For most teams the second answer is cheaper within a single quarter.
How can I help you explore Laravel packages today?