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 Eloquent Position Laravel Package

pion/laravel-eloquent-position

A Laravel Eloquent package that adds sortable position columns to your models. Automatically manage record ordering, move items up, down, or to a specific position, and query models in sequence with minimal setup. Useful for drag-and-drop lists, menus, and any ordered data.

View on GitHub
Deep Wiki
Context7

Getting Started

The package is deliberately small: one trait that keeps an integer position column in sync and exposes move operations. Because it has a single maintainer, nine stars and no dependents, treat the source in src/ as the documentation of record — read the trait before wiring it into anything important, and confirm the exact trait name and method signatures against the README for the version you install.

1. Install and add the column. The package does not ship a migration; you own the schema.

composer require pion/laravel-eloquent-position
Schema::table('articles', function (Blueprint $table) {
    $table->unsignedInteger('position')->default(0);
    $table->index(['category_id', 'position']); // scope + sort
});

2. Attach the trait and declare the ordering scope.

use Pion\Laravel\EloquentPosition\HasPosition; // verify namespace/trait name in the README

class Article extends Model
{
    use HasPosition;

    protected $positionColumn = 'position';
    protected $positionGroup  = ['category_id']; // rows are ordered *within* this group
}

3. First use case — a per-category list with up/down buttons. Read the list with ->orderBy('position'), and expose moveUp() / moveDown() (or whatever the trait names them) on the model. That single scenario exercises everything the package does: append-on-create, scoped numbering, and shifting neighbours.

Where to look first, in order: the trait's boot method (what it hooks into creating/deleting), the group/column properties, then the tests — they are the fastest way to see intended semantics for edge cases like "move first item up".

Implementation Patterns

Append-on-create with a scope. Let the trait assign the position on creating rather than computing max(position) + 1 yourself; that's the whole point of the package. Anything that bypasses Eloquent events (seeders using insert(), raw SQL, upsert) silently produces duplicates at position 0 — see Gotchas.

Controller endpoints for reordering. Keep the model thin and put authorisation in front of every mutation:

public function moveUp(Article $article)
{
    $this->authorize('update', $article);
    $article->moveUp();          // re-reads siblings inside the group
    return back();
}

Drag-and-drop / explicit target position. For a full reorder from a JS table, prefer one transactional endpoint that accepts an ordered array of IDs and writes positions in sequence, rather than N calls to moveToPosition(). N calls means N shifts and N round-trips:

DB::transaction(function () use ($ids, $categoryId) {
    Article::where('category_id', $categoryId)
        ->whereIn('id', $ids)
        ->lockForUpdate()
        ->get();

    foreach ($ids as $i => $id) {
        Article::whereKey($id)->update(['position' => $i + 1]);
    }
});

Scoped lists as a first-class concept. If you have several boards/categories on one page, group the queries by scope and render each list independently — never mix a global orderBy('position') with per-group moves, or the UI will disagree with the DB.

Pair with an API resource. Expose position read-only and add a can_move_up/can_move_down flag computed from min/max in the group so the client doesn't render dead buttons.

Cache invalidation. If you cache an ordered list, flush on create, delete and move — moves typically don't touch updated_at in a way your observers will catch.

Gotchas and Tips

  • Don't put a unique index on (group, position). A swap or shift transiently duplicates positions and will blow up mid-transaction on MySQL/Postgres. Use a plain index; enforce uniqueness in application code under a lock, or use a deferred constraint if your driver supports it.
  • Concurrency. Two simultaneous creates in the same group can compute the same next position. Wrap creates in a transaction with lockForUpdate() on the sibling rows, or accept occasional duplicate positions and normalise on read (orderBy('position')->orderBy('id')) so the list stays deterministic.
  • Bulk writes bypass the trait. Model::insert(), upsert(), update() on a query builder, and ->each() over a chunk without saving all skip the creating hook. Seeders are the classic offender: seed sequentially with create() or assign positions explicitly.
  • Gaps and soft deletes. Deleting a row leaves a hole; if you rely on position == count assumptions in the UI, add a normalisation command (php artisan positions:compact) rather than renumbering on every delete.
  • Keep position out of $fillable. Otherwise a crafted request can pin a record to an arbitrary slot and create duplicates.
  • Reordering is O(n) writes. Each move shifts the rows between source and target. Fine for tens of rows per group, wrong for thousands — if a group grows past a few hundred, switch to spaced integers (1000, 2000, …) or fractional/lexicographic ranks.
  • Aggregate queries need the scope. min('position') / max('position') must include the same group where clauses the trait uses, or "is this the first item?" checks lie.
  • Debugging. Log DB::listen() while calling a move and read the generated UPDATEs — that's the fastest way to see which group predicates the trait actually applies. Test against SQLite in-memory for wiring, but verify reorder behaviour on your production driver: SQLite is lenient about constraints and NULL ordering in ways MySQL isn't.
  • Version risk. Last release is July 2021, so check composer.json constraints against your Laravel/PHP version before committing; you may need "ignore-platform-reqs" or a fork. Pin the version in composer.json and cover the trait with your own feature tests so a future Laravel upgrade surfaces breakage in CI rather than in the reorder button.
  • Extension points. The column and group are plain properties on the model — override them per model rather than forking the package. If you need custom semantics (e.g. moving across groups), implement it in a model method that wraps the trait's primitives inside your own transaction instead of editing vendor code.
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.
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
frankyso/ipaymu
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core