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

Orderly Laravel Package

baril/orderly

Orderly adds orderable/sortable behavior to Laravel Eloquent models. Add a position column and the Orderable trait to reorder records with methods like moveToOffset, using a custom order column if needed.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package and verify the Laravel compatibility table before pinning a version. Newer Laravel releases require newer Orderly majors/minors: Laravel 13 → 3.4+, 12 → 3.3+, 11 → 3.2+, 10 → 3.1+, 9 → 3.x, 8 → 2.x/3.x, 7/6 → 1.x.

composer require baril/orderly

If package discovery is disabled, register the provider:

'providers' => [
    Baril\Orderly\OrderlyServiceProvider::class,
],

Add an unsigned integer ordering column to the table:

Schema::create('articles', function (Blueprint $table) {
    $table->id();
    $table->string('title');
    $table->unsignedInteger('position');
    $table->timestamps();
});

Then use the trait and guard the position field so it is not mass-assigned:

use Baril\Orderly\Concerns\Orderable;

class Article extends Model
{
    use Orderable;

    protected $guarded = ['position'];
}

If the column is not named position, set $orderColumn:

protected $orderColumn = 'order';
protected $guarded = ['order'];

First useful workflow: create records without manually calculating position, then read them in order.

$article = Article::create([
    'title' => $request->input('title'),
    'body' => $request->input('body'),
]);
// position is automatically MAX(position) + 1

$articles = Article::ordered()->get();
$latest = Article::ordered('desc')->get();

Implementation Patterns

Single-model moves are the day-to-day API. All of these save the model automatically, so you do not call save() afterward.

$article->moveToStart();
$article->moveToEnd();
$article->moveToPosition(3);       // 1-based, must be valid
$article->moveToOffset(0);         // 0-based
$article->moveToOffset(-1);        // last position
$article->moveUp();
$article->moveDown(2);
$article->moveUp(2, false);        // clamp at boundary instead of throwing
$article->swapWith($other);
$article->moveBefore($other);
$article->moveAfter($other);

Ordered queries and adjacency lookups. Use the ordered scope instead of raw orderBy('position') so grouping and relation ordering work consistently.

$articles = Article::ordered()->get();
$articles = Article::ordered('desc')->get();
$articles = Article::unordered()->get();

$article = Article::find(10);
$next = $article->next()->first();
$previousFive = $article->previous(5)->get();
$nextFive = $article->next(5)->get();

Mass reordering. Avoid looping over moveToOffset() unless each model is freshly loaded; otherwise stale positions corrupt the result. Prefer the collection helpers.

$collection = Article::orderBy('publication_date', 'desc')->get();
$collection->saveOrder(); // applies collection order to position column

setOrder() on the custom collection saves the new order immediately. It reorders only the models present in that collection; other rows are untouched.

$ordered = $collection->setOrder([4, 5, 2]);

You can also call setOrder statically or on a query builder. The return value is the number of affected rows.

Status::setOrder([2, 1, 5, 3, 4]);      // reorder all statuses
Status::setOrder([4]);                 // move id 4 to the front, adjust others
Status::whereKey([3, 4, 5])->setOrder([4, 5, 3]); // reorder only that subset

Grouped ordering / orderable one-to-many relations. When each parent has its own ordering, set $groupColumn.

class Article extends Model
{
    use Orderable;

    protected $guarded = ['position'];
    protected $groupColumn = 'section_id';
}

Multiple grouping columns are supported:

protected $groupColumn = ['tenant_id', 'section_id'];

Then a parent relation can expose ordered children:

class Section extends Model
{
    public function articles()
    {
        return $this->hasMany(Article::class)->ordered();
    }
}

Orderable many-to-many and morph-to-many relations. The pivot table needs a position column. Use HasOrderableRelationships on the parent model.

use Baril\Orderly\Concerns\HasOrderableRelationships;

class Post extends Model
{
    use HasOrderableRelationships;

    public function tags()
    {
        return $this->belongsToManyOrderable(Tag::class);
    }
}

belongsToManyOrderable() takes the order column as its second parameter if the pivot column is not position. Attach, sync, and detach maintain pivot positions.

$post->tags()->attach($tag->id);       // appends
$post->tags()->sync([$tag1->id, $tag2->id]); // preserves supplied order
$post->tags()->detach($tag->id);       // decrements later positions

$ordered = $post->tags()->ordered()->get();
$reverse = $post->tags()->ordered('desc')->get();

Use belongsToManyOrdered() when the relation should be ordered by default. Relation-level move methods require the related model as the first argument.

$post->tags()->moveBefore($tag1, $tag2);
$post->tags()->moveAfter($tag1, $tag2);
$post->tags()->moveToStart($tag1);

$first = $post->tags()->ordered()->first();
$second = $post->tags()->after($first)->first();

Morph variants use morphToManyOrderable() and morphedByManyOrderable(); the third parameter names the order column:

return $this->morphToManyOrderable(Tag::class, 'taggable', 'tag_order');
return $this->morphedByManyOrderable(Post::class, 'taggable', 'order');

Maintenance workflow. If rows are deleted outside Eloquent or positions become inconsistent, repair them with the Artisan command.

php artisan orderly:fix-positions "App\\Models\\Article"
php artisan orderly:fix-positions "App\\Models\\Post" tags

Transaction pattern. For user-facing reorder screens, wrap the operation in a transaction and use saveOrder() / setOrder() rather than a loop of individual moves.

DB::transaction(function () use ($collection) {
    $collection->saveOrder();
});

Gotchas and Tips

  • Do not make position fillable. Add it to $guarded or leave it out of $fillable. The package manages it through create/delete events and move methods.
  • Mass moves need fresh models. $model->moveToOffset($key) on a stale collection can corrupt ordering. Use fresh() per model only as a fallback; prefer saveOrder() or setOrder().
  • Collection setOrder() saves automatically. You do not need to call saveOrder() afterward. It also only reorders models within the collection, not every row in the table.
  • Static/query-builder setOrder() semantics differ by query shape. A full-table static call reorders all rows; a whereKey(...) query only swaps/reorders that subset and returns affected row count.
  • Many-to-many methods are relation methods, not model methods. Use $post->tags()->moveBefore($tag1, $tag2), not $tag1->moveBefore($tag2). Passing a model that does not belong to the relation throws Baril\Orderly\GroupException.
  • Default-ordered relations ignore later orderBy() calls. If the relation is defined with belongsToManyOrdered(), use unordered()->orderBy('id') or forceOrderBy('id') to order by another column.
  • Move method boundaries can throw. moveToPosition() requires a valid position. moveUp() / moveDown() throw PositionException when strict mode is enabled; pass false to clamp to the first/last position instead.
  • Offsets are 0-based; positions are 1-based. Negative offsets count from the end (-1 is last). This is a common source of off-by-one errors.
  • Bulk query-builder deletes bypass model events. Deleting through Eloquent fires the delete event and closes position gaps; DB::table(...)->delete() or mass query deletes may leave gaps. Run orderly:fix-positions afterward.
  • Database support changed over time. Version 3.3+ supports SQLite, PostgreSQL, and SQL Server; earlier versions were MySQL-only. Check your installed version before deploying.
  • Concurrent creates can race on MAX(position) + 1. If ordering must be strictly correct under concurrent writes, wrap creation/reordering in transactions and consider locking strategies or a unique constraint appropriate to your grouping.
  • Low adoption means test your stack. The package is small and has few dependents; validate migrations, grouped ordering, and relation behavior on your exact Laravel and database versions. Pin a compatible version and include reorder tests in your suite.
  • Extension points are primarily declarative. Use $orderColumn, $groupColumn (including arrays), the OrderableCollection, and the orderable relation classes/methods. Custom behavior generally belongs in your own service/action layer around transactions and saveOrder() / setOrder() rather than patching move methods.
  • Debugging ordering issues starts with the raw column. Inspect position values per group, check whether deletes happened outside Eloquent, confirm the model was fresh before a manual move, and run orderly:fix-positions in non-production first.
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.
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
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log