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.
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();
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();
});
position fillable. Add it to $guarded or leave it out of $fillable. The package manages it through create/delete events and move methods.$model->moveToOffset($key) on a stale collection can corrupt ordering. Use fresh() per model only as a fallback; prefer saveOrder() or setOrder().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.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.$post->tags()->moveBefore($tag1, $tag2), not $tag1->moveBefore($tag2). Passing a model that does not belong to the relation throws Baril\Orderly\GroupException.orderBy() calls. If the relation is defined with belongsToManyOrdered(), use unordered()->orderBy('id') or forceOrderBy('id') to order by another column.moveToPosition() requires a valid position. moveUp() / moveDown() throw PositionException when strict mode is enabled; pass false to clamp to the first/last position instead.-1 is last). This is a common source of off-by-one errors.DB::table(...)->delete() or mass query deletes may leave gaps. Run orderly:fix-positions afterward.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.$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.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.How can I help you explore Laravel packages today?