Installation:
composer require alexcrawford/lexorank-sortable
Ensure compatibility with your Laravel version (see version table).
Add the trait and column:
use AlexCrawford\Sortable\Sortable;
class Article extends Model
{
use Sortable;
protected $sortable = [
'column' => 'position', // Default column name
'order' => 'asc', // Default order (asc/desc)
];
}
Run a migration to add the position column (lexorank string type):
Schema::create('articles', function (Blueprint $table) {
$table->lexorank('position')->nullable(); // or ->default('0')
});
First use case:
// Create and sort items
$article = Article::create(['title' => 'First Article']);
$article->setPositionBefore($article->fresh()); // Insert before itself (top)
// Query sorted results
$sortedArticles = Article::sorted()->get();
// Insert at top
$item->setPositionBefore($item->fresh());
// Insert at bottom
$item->setPositionAfter($item->fresh());
// Insert between two items
$item->setPositionBetween($item1, $item2);
$item->moveUp(); // Move up one position
$item->moveDown(); // Move down one position
// Default order (asc)
$items = Article::sorted()->get();
// Custom order (desc)
Article::sorted('desc')->get();
// Scoped queries
Article::where('published', true)->sorted()->get();
Article::sorted()->paginate(10);
For many-to-many or grouped relationships:
// Model: Article (belongsToMany Tags)
class Article extends Model
{
use Sortable;
protected $sortable = [
'column' => 'position',
'group' => 'tag_id', // Group by tag_id
];
}
// Usage:
$tag = Tag::find(1);
$tag->articles()->sorted()->get(); // Sorted by tag_id
// Reorder multiple items
$items = Article::all();
$items->each(function ($item) {
$item->setPositionBefore($item->fresh());
});
Leverage sorted() in API responses:
return Article::sorted()->get()->map(function ($item) {
return [
'id' => $item->id,
'title' => $item->title,
'order' => $item->position, // Optional: include position in response
];
});
sorted() in resource queries:
public function query()
{
return Article::sorted();
}
Listen for position changes:
// app/Providers/EventServiceProvider.php
protected $listen = [
'AlexCrawford\Sortable\Events\PositionUpdated' => [
'App\Listeners\LogPositionChange',
],
];
Override default position column:
protected $sortable = [
'column' => 'sort_order',
];
Lexorank Column Type:
string instead of lexorank in migrations.$table->lexorank('column_name').lexorank cast isn’t available, use string and manually handle lexorank logic.Grouping Without Foreign Key:
group without a foreign key column.group value matches a column in the pivot/table (e.g., tag_id for grouped sorting).Concurrent Updates:
DB::transaction(function () use ($item) {
$item->moveUp();
});
Performance with Large Datasets:
sorted() queries can be slow on tables with millions of rows.position column.limit() or take() for pagination:
Article::sorted()->take(50)->get();
Invalid Lexorank Values:
SQLSTATE[22007]: Invalid datetime format.null or non-lexorank values).protected $casts = [
'position' => 'lexorank',
];
Grouping Not Working:
group value in $sortable matches a column in the query scope.toSql() on the query to inspect the generated SQL:
$query = Article::where('tag_id', 1)->sorted();
dd($query->toSql());
Position Not Persisting:
setPositionBefore() doesn’t update the database.save() after setting position:
$item->setPositionBefore($otherItem);
$item->save(); // Required!
saving observer).Custom Lexorank Logic: Override the lexorank generator:
use AlexCrawford\Sortable\Lexorank;
class CustomLexorank extends Lexorank
{
public function generate(): string
{
// Custom logic
return parent::generate();
}
}
// In your model:
protected $sortable = [
'lexorank' => CustomLexorank::class,
];
Adding Sortable to Non-Eloquent Models: Use the standalone lexorank class:
use AlexCrawford\Sortable\Lexorank;
$lexorank = new Lexorank();
$position = $lexorank->generate();
Custom Query Scopes:
Extend the sorted() scope:
class Article extends Model
{
public function scopeCustomSorted($query)
{
return $query->sorted()->where('published', true);
}
}
Testing:
Use the Lexorank class directly in tests:
public function testLexorankGeneration()
{
$lexorank = new Lexorank();
$this->assertEquals('0', $lexorank->generate());
$this->assertEquals('1', $lexorank->generate());
}
Seed Data with Sorted Order:
public function run()
{
$articles = collect(['A', 'B', 'C'])->map(fn ($title) => [
'title' => $title,
'position' => null, // Let lexorank handle it
]);
Article::insert($articles->toArray());
}
Frontend Drag-and-Drop: Use a library like interact.js to capture drag events and update positions via AJAX:
interact('.sortable-item').draggable({
onmove: dragEvent => {
const draggable = dragEvent.target;
const rect = draggable.getBoundingClientRect();
// Send position updates to Laravel
}
});
Soft Deletes:
Lexorank works with soft deletes, but ensure your sorted() queries include withTrashed() if needed:
Article::withTrashed()->sorted()->get();
Performance Optimization: Cache sorted queries if they rarely change:
$sorted = Cache::remember('sorted_articles', now()->addHours(1), function () {
return Article::sorted()->get();
How can I help you explore Laravel packages today?