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

Eloquent Rankable Laravel Package

rtablada/eloquent-rankable

A lightweight Laravel trait that adds sortable rank to Eloquent models. Keep an order in a rank column, move records up or down, and fetch ranked lists without writing custom SQL. Simple trait-based setup for menus, galleries, and any admin-sorted content, with no complex configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

  1. Install and sanity-check the source first. composer require rtablada/eloquent-rankable. With 31 stars, zero dependents, and no recent activity, open vendor/rtablada/eloquent-rankable/src/ before writing any code — the trait is small, so read it top to bottom and confirm the method names and default column it expects. Do not rely on the README alone.
  2. Add the column via your own migration. The package does not ship migrations. Add an unsigned integer rank (default 0) to the table and index it, ideally as a composite index with whatever your grouping column is (['board_id', 'rank']).
  3. Apply the trait to the model. use Rtablada\EloquentRankable\Rankable; inside the model. Override the column name and grouping hooks on the model itself if the package exposes them (typically a $rankColumn property and a rankingGroup()/scope method) — check the trait for the exact names in your pinned version.
  4. First use case: an admin-managed ordered list. A FAQ list, menu items, kanban cards within a column, or homepage blocks. Create three records, call the "move up" method on the third, and confirm the persisted rank values swap rather than renumber the whole table. That single test tells you whether the package fits your workload.
  5. Where to look first: the trait's boot/creating hooks (does it auto-assign rank on insert?), its move methods, and whether reordering is wrapped in a transaction internally or expects you to provide one.

Implementation Patterns

Append-on-create. Confirm whether the trait assigns a rank automatically when a model is created. If it does not, set it yourself in a creating observer or model event:

static::creating(function ($model) {
    $model->rank = (static::where('board_id', $model->board_id)->max('rank') ?? 0) + 1;
});

Without this you get a table full of rank = 0 and arbitrary display order — the single most common first-day failure.

Scoped ranking per group. For anything with a parent (board, category, tenant), make the grouping explicit so each group has its own independent 1..n sequence. Keep the group key in the model's group hook rather than passing it at every call site, and always filter by it when rendering:

$cards = Card::where('board_id', $board->id)->orderBy('rank')->get();

Controller endpoint for drag-and-drop. Model the move as a single PATCH with a target position, not a bulk "save the whole array" payload:

public function move(Request $request, Card $card)
{
    $request->validate(['position' => ['required', 'integer', 'min:0']]);

    DB::transaction(function () use ($card, $request) {
        $card->insertAt((int) $request->position);
    });

    return back();
}

insertAt() is usually the only method you need in the UI layer; moveUp/moveDown/moveToTop/moveToBottom are conveniences for buttons. Wrapping in a transaction is non-negotiable — a partially applied reorder produces duplicate ranks and a visibly broken list.

Bulk reorder from a submitted array. If your frontend posts an ordered list of IDs (common with SortableJS), loop within one transaction and treat index as the position. Keep the loop idempotent so a double-submit does not corrupt ranks.

Cache invalidation. Any response cache or view cache keyed on the list must be flushed in the same place you reorder. Put the flush in a model event or in the controller's transaction block so it can't drift from the write.

Read-side ordering. Standardise on ->orderBy('rank') everywhere, and add a small local scope (scopeRanked) on your model rather than relying on a package scope — it survives a future migration off this package.

Gotchas and Tips

  • Maintenance risk is the headline gotcha. Zero dependents and a low activity score mean you are effectively vendoring this. Pin the exact version in composer.json, and write your reordering logic against a thin wrapper class (or a Rankable concern of your own) so swapping to spatie/eloquent-sortable, which is actively maintained and does essentially the same job, is a one-file change.
  • Duplicate ranks are silent and cumulative. A failed move, a manual UPDATE, or a concurrent reorder can leave two rows with the same rank; SQL then returns them in arbitrary order and the bug looks like "the list reshuffles on every refresh". Add a scheduled or artisan command that detects duplicates and renumbers per group, and add a composite unique index only if every reorder is transactional — otherwise the unique constraint will make moves fail halfway.
  • Gaps vs. contiguous ranks. If you only ever sort relatively, gaps are harmless and cheaper to maintain. The move methods may assume contiguous 1..n; verify against the source before letting anyone insert rows outside the package.
  • Cost is O(n) per move. A move typically updates a range of rows. Fine for dozens of items, painful for thousands. Past a few hundred per group, switch to fractional/LexoRank-style keys and accept that this package is the wrong tool.
  • Concurrency. Two admins reordering the same board simultaneously will interleave updates. Use lockForUpdate() on the group's rows inside the transaction if reordering is a real multi-user workflow.
  • Soft deletes and global scopes. Soft-deleted rows still occupy a rank. Either exclude them from the group query consistently in both the package calls and your own reads, or renumber on delete — mixed behaviour produces "phantom" gaps and off-by-one moves.
  • Mass assignment. Keep rank out of $fillable. A stray $request->all() in an update path will let clients rewrite ordering directly and bypass the move methods.
  • DB-specific null ordering. MySQL and SQLite sort NULL ranks differently in ORDER BY. Backfill rank to a non-null default before going live, and never leave the column nullable.
  • Test relative order, not absolute ranks. Assert pluck('id') sequences after a move; asserting exact rank integers makes tests brittle the moment you change the renumbering strategy. Use RefreshDatabase and seed three to five items per test.
  • Debugging: wrap reorders in DB::listen() and dump the statements — it is the fastest way to see whether the package is doing a full-table renumber or a targeted swap, which determines whether you can keep it at your scale.
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.
steadrun/laravel-monitor
labyrinthe/connect-laravel
elmogy/layan
drewdan/sentinel-client
dskripchenko/laravel-llm
fatryst/nuonuo
stevenay/myanfont
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin