# Eloquent Sortable Eloquent Sortable is a Laravel package that provides sortable behavior for Eloquent models. It automatically manages an order column on your models, assigning sequential order values when new records are created and providing methods to reorder, move, and swap model positions. The package offers a trait-based implementation that integrates seamlessly with Laravel's Eloquent ORM. It supports soft deletes, grouped sorting (e.g., sorting within a parent relationship), and dispatches events after sorting operations. The package is compatible with Laravel 9.x through 12.x and requires PHP 8.1+. ## Installation Install via Composer and optionally publish the configuration file. ```bash # Install the package composer require spatie/eloquent-sortable # Optionally publish the config file php artisan vendor:publish --tag=eloquent-sortable-config ``` ## Basic Model Setup Implement the `Sortable` interface and use the `SortableTrait` to add sortable behavior to any Eloquent model. ```php 'order_column', 'sort_when_creating' => true, ]; } // Creating models automatically assigns order values $task1 = Task::create(['name' => 'First task']); // order_column = 1 $task2 = Task::create(['name' => 'Second task']); // order_column = 2 $task3 = Task::create(['name' => 'Third task']); // order_column = 3 // Retrieve records in order $orderedTasks = Task::ordered()->get(); // Returns: First task (1), Second task (2), Third task (3) ``` ## ordered() - Query Scope for Ordered Results The `ordered()` scope returns models sorted by their order column. Accepts an optional direction parameter. ```php get(); // Get all tasks in descending order $tasksDesc = Task::ordered('desc')->get(); // Combine with other query conditions $completedTasks = Task::where('status', 'completed') ->ordered() ->get(); // Paginate ordered results $paginatedTasks = Task::ordered()->paginate(10); ``` ## setNewOrder() - Bulk Reorder by IDs The `setNewOrder()` method reorders all records based on an array of IDs. The first ID gets order 1, second gets order 2, etc. ```php pluck('id')->shuffle(); Task::setNewOrder($ids); // Modify the query (e.g., bypass global scopes) Task::setNewOrder([3, 1, 2], 1, null, function ($query) { $query->withoutGlobalScope('ActiveScope'); }); ``` ## setNewOrderByCustomColumn() - Reorder by Custom Column Use `setNewOrderByCustomColumn()` when your identifier is not the primary key (e.g., UUID). ```php moveOrderDown(); // $task now has order_column = 4, previous order 4 model now has order 3 // Move up one position (swap with order 3) $task->moveOrderUp(); // $task now has order_column = 3 // Methods return $this for chaining, and safely handle edge cases $firstTask = Task::ordered()->first(); $firstTask->moveOrderUp(); // No change, already at top $lastTask = Task::ordered('desc')->first(); $lastTask->moveOrderDown(); // No change, already at bottom ``` ## moveToStart() / moveToEnd() - Move to Extreme Position Move a model to the very beginning or end of the ordered list. ```php moveToStart(); // Result: C(1), A(2), B(3), D(4), E(5) // Move to last position $taskC->moveToEnd(); // Result: A(1), B(2), D(3), E(4), C(5) // Methods return $this for chaining $task = Task::find(1) ->moveToEnd() ->update(['highlighted' => true]); ``` ## moveAfter() / moveBefore() - Move Relative to Another Model Position a model directly after or before a specific target model. ```php moveAfter($taskB); // Result: A(1), B(2), E(3), C(4), D(5) // Move E directly before B $taskE->moveBefore($taskB); // Result: A(1), E(2), B(3), C(4), D(5) // Useful for drag-and-drop reordering $draggedTask = Task::find($request->dragged_id); $targetTask = Task::find($request->target_id); if ($request->position === 'after') { $draggedTask->moveAfter($targetTask); } else { $draggedTask->moveBefore($targetTask); } ``` ## swapOrder() - Swap Two Models Exchange the order positions of two models. ```php swapOrderWithModel($task2); // Useful for "move up/down" UI buttons that need to swap $currentTask = Task::find($id); $nextTask = Task::where('order_column', '>', $currentTask->order_column) ->ordered() ->first(); if ($nextTask) { Task::swapOrder($currentTask, $nextTask); } ``` ## isFirstInOrder() / isLastInOrder() - Position Check Check if a model is at the first or last position in the order sequence. ```php isFirstInOrder()) { // Disable "Move Up" button in UI } if ($task->isLastInOrder()) { // Disable "Move Down" button in UI } // Useful for conditional UI rendering $tasks = Task::ordered()->get()->map(function ($task) { return [ 'id' => $task->id, 'name' => $task->name, 'canMoveUp' => !$task->isFirstInOrder(), 'canMoveDown' => !$task->isLastInOrder(), ]; }); ``` ## getHighestOrderNumber() / getLowestOrderNumber() - Order Boundaries Get the current maximum or minimum order value in the table. ```php getHighestOrderNumber(); // e.g., 25 // Get the lowest order number currently in use $minOrder = $task->getLowestOrderNumber(); // e.g., 1 // Useful for manual order assignment $newTask = new Task(['name' => 'New task']); $newTask->order_column = $task->getHighestOrderNumber() + 1; $newTask->save(); ``` ## buildSortQuery() - Grouped Sorting Override `buildSortQuery()` to scope sorting within groups (e.g., tasks within a project). ```php where('project_id', $this->project_id); } } // Now each project has independent ordering $project1Task = Task::create(['name' => 'Task A', 'project_id' => 1]); // order = 1 in project 1 $project1Task2 = Task::create(['name' => 'Task B', 'project_id' => 1]); // order = 2 in project 1 $project2Task = Task::create(['name' => 'Task C', 'project_id' => 2]); // order = 1 in project 2 // Moving within a project only affects that project's tasks $project1Task->moveOrderDown(); // Swaps with Task B, doesn't affect project 2 // Get ordered tasks for a specific project $project1Tasks = Task::where('project_id', 1)->ordered()->get(); ``` ## EloquentModelSortedEvent - Sort Event Listener Listen for the `EloquentModelSortedEvent` dispatched after sorting operations to trigger post-sort actions. ```php isFor(Task::class)) { Cache::forget('tasks.ordered'); Cache::forget('tasks.homepage'); } // Access the model class name $modelClass = $event->model; // e.g., "App\Models\Task" } } // Register in EventServiceProvider protected $listen = [ \Spatie\EloquentSortable\EloquentModelSortedEvent::class => [ \App\Listeners\ClearTaskCacheListener::class, ], ]; ``` ## Configuration Options Configure default behavior via config file or model properties. ```php 'order_column', // Auto-assign order when creating new models 'sort_when_creating' => true, // Don't update updated_at when reordering 'ignore_timestamps' => false, ]; // Override per model class Task extends Model implements Sortable { use SortableTrait; public $sortable = [ 'order_column_name' => 'position', // Use 'position' column 'sort_when_creating' => false, // Don't auto-assign on create ]; } // Disable auto-sorting for bulk imports class ImportedTask extends Model implements Sortable { use SortableTrait; public $sortable = [ 'sort_when_creating' => false, ]; } // After import, manually set order ImportedTask::setNewOrder(ImportedTask::pluck('id')->toArray()); ``` ## Summary Eloquent Sortable is ideal for any Laravel application that needs to maintain ordered lists of records, such as task managers, content management systems with sortable pages or menu items, e-commerce product catalogs, playlist managers, or any drag-and-drop reordering interface. The package handles all the complexity of maintaining sequential order values, including shifting other records when items are moved. Integration is straightforward: add an integer `order_column` to your migration, implement the `Sortable` interface, use the `SortableTrait`, and optionally override `buildSortQuery()` for grouped sorting. The package works seamlessly with soft deletes, global scopes, and custom primary keys (UUIDs). For frontend integration, combine the `setNewOrder()` method with a JavaScript sortable library to implement drag-and-drop reordering with minimal backend code.