oddvalue/eloquent-sortable
A Laravel package that adds sortable behavior to Eloquent models. Use a simple trait to assign and maintain custom order, move records up or down, and reorder items reliably—great for drag-and-drop lists, menus, and other ordered content.
What it is. oddvalue/eloquent-sortable is a Laravel trait that maintains a user-defined integer ordering column on Eloquent models — automatic renumbering on create/update/delete, move-before/move-after/move-to-start/move-to-end helpers, an ordered() query scope, and scoped (grouped) ordering so independent lists (per category, per tenant) each keep their own sequence. The description positions it as additional features on top of spatie/eloquent-sortable, so the honest framing for planning purposes is: a small, focused fork of a well-established utility, not a novel platform.
Architecture fit. Integration surface is a trait plus one integer column — no service provider, no config publishing, no custom query builder, no schema rewriting. That is the lowest-coupling shape available and keeps blast radius to the models we explicitly opt in. It solves flat, optionally grouped lists only. It is not a tree/nested-set implementation and ships no drag-and-drop UI. If the roadmap includes hierarchical menus or category trees, this covers a fraction and a second mechanism will be needed.
Release assessment (v2.2.0). The delta from v2.1.0 is four Dependabot PRs touching only GitHub Actions versions (dependabot/fetch-metadata, aglipanci/laravel-pint-action, ramsey/composer-install). No source changes, no public API changes, no deprecations, no schema or config changes, no Composer dependency changes. Functionally this is a CI-maintenance release; upgrade risk from v2.1.x is negligible and the effort is lockfile-only.
Technical risk — this is where a TPM earns their keep:
spatie/eloquent-sortable upstream; the fork must justify itself with a feature we actually need.Key questions to resolve before adoption.
^2.2 support, and does it cover our target (Laravel 11/12, PHP 8.2+)? Verify from composer.json, not the changelog.spatie/eloquent-sortable, and do we actually require any of them? If not, why carry the fork?company_id + order) cleanly, and what happens when a model moves between groups?position column, in case we start there and swap later?Stack fit. Drop-in for a standard Laravel app: composer require oddvalue/eloquent-sortable:^2.2, add the trait to the target models, add the ordering column via migration, configure grouping on the model. No bindings, no facades, no config publishing. It coexists with policies, observers, and API resources without interference — which means it will not block or complicate any parallel workstream.
Migration path.
ROW_NUMBER() OVER (PARTITION BY <group> ORDER BY created_at, id)), then set NOT NULL.(group_column, order_column) — the single highest-value step for both ordered() reads and move performance.ReorderService (or per-model action) that wraps every move in DB::transaction() with lockForUpdate() on the affected group, and is the only sanctioned entry point for reordering. This isolates the package and gives us a swap-out seam if the fork is abandoned.Compatibility. Pin to ^2.2.0 and commit the lockfile. Because the release contains no source changes, upgrading from 2.1.x is a lockfile-only change — bundle it with routine dependency maintenance rather than scheduling a dedicated window. If starting fresh, adopt 2.2.0 directly.
Sequencing. Schema + backfill + index (one PR, reversible) → service wrapper + tests (one PR) → API endpoint + authorization (one PR) → UI. Do not ship the UI before the transactional endpoint exists, or the first concurrent drag becomes a data-repair ticket. Total engineering shape: small, incremental, no cross-team dependencies — which is exactly the profile that should be scheduled as filler work alongside a larger initiative rather than as a standalone milestone.
Maintenance. Very low ongoing effort: one Composer dependency, no runtime configuration, no background jobs, no external services, no infra. The recurring task is watching upstream for Laravel major-version support. Given the maintenance-mode signal in this release and the 2-star fork lineage, budget a documented fallback: our wrapper makes a fork or an in-house trait a contained change of roughly a day. Enable Dependabot/Renovate on our side and treat this as a patch-level auto-merge candidate once the wrapper's tests are green.
Support. Community-only: GitHub issues, no SLA, no vendor, effectively one maintainer. Acceptable for a utility trait whose failure mode is contained to ordering and not availability — but it must be recorded in the dependency register as an unsupported component with an owner. We own the semantics we depend on via our wrapper's test suite (moves within group, first/last, cross-group move, empty group, delete-and-renumber).
Scaling. Ordering cost scales with group size, not table size — grouped scoping is what keeps this viable for multi-tenant or per-category lists. The composite index makes ordered() reads cheap. The realistic ceiling is tens of thousands of rows per group per move; beyond that, plan a sparse-ordering variant (large gaps plus a periodic renumber job) or a dedicated position table. Read traffic is unaffected, so browse/list performance scales normally.
Failure modes to monitor.
COUNT(*) vs MAX(order) mismatch per group) and alert.Deadlock found errors and retry with jitter.Ramp-up. Roughly half a day for a mid-level Laravel engineer to be productive: the API is a handful of methods and one trait. The real learning is not the package but our transactional reorder endpoint and the ordering invariants. Write one page documenting column, index, wrapper, invariants, and integrity check, so the second engineer does not re-derive them. No specialised skills, no infra change, no on-call additions.
Recommendation. Adopt v2.2.0, pinned, behind a thin service wrapper, with the composite index and a transactional reorder endpoint as non-negotiable prerequisites. The release itself is a no-op for our code and should be consumed as routine lockfile maintenance. The decision rests on architectural fit and maintenance-mode tolerance, not on this changelog — and the fork question (build on this vs. go upstream to spatie/eloquent-sortable) should be answered explicitly and recorded before the first PR merges.
How can I help you explore Laravel packages today?