alik-laravel / ordering
Laravel package for ordering functionality
Requires
- php: ^8.2
- illuminate/contracts: ^10.0|^11.0|^12.0
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.0
- nunomaduro/collision: ^7.8|^8.0
- orchestra/testbench: ^8.8|^9.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- rector/rector: ^2.0
This package is not auto-updated.
Last update: 2025-09-18 11:39:28 UTC
README
A simple Laravel package that provides ordering functionality for Eloquent models. Add the HasOrdering
trait to any model to automatically handle ordering with scope support.
Installation
You can install the package via composer:
composer require alik-laravel/ordering
Requirements
- PHP 8.2+
- Laravel 10.0+ or 11.0+
Usage
1. Add the trait to your model
use Alik\Ordering\HasOrdering; use Illuminate\Database\Eloquent\Model; class MenuItem extends Model { use HasOrdering; protected $fillable = ['name', 'order', 'category_id']; /** * @return array<string, string> */ protected function casts(): array { return [ 'order' => 'float', ]; } }
2. Add an order
column to your migration
Schema::create('menu_items', function (Blueprint $table) { $table->id(); $table->string('name'); $table->float('order')->default(0); $table->unsignedBigInteger('category_id')->nullable(); $table->timestamps(); });
3. Automatic ordering
Models with the HasOrdering
trait will:
- Automatically be ordered by the
order
column - Get the next available order value when created
- Provide methods for reordering
// Models are automatically ordered $items = MenuItem::all(); // Ordered by 'order' column ASC // New models get the next order automatically $newItem = MenuItem::create(['name' => 'New Item']); // order = last_order + 1
4. Manual ordering operations
// Get an Orderer instance for positioning $item = MenuItem::find(1); $orderer = $item->ordering(); // Calculate positions $beforePosition = $orderer->before(); // Position before current item $afterPosition = $orderer->after(); // Position after current item $firstPosition = $orderer->first(); // First position $lastPosition = $orderer->last(); // Last position // Reorder all items (fixes gaps in ordering) MenuItem::reorder(); // Reorder within a scope (e.g., same category) MenuItem::reorderWithinScope(['category_id' => 1]);
5. Scoped ordering
You can work with ordering within specific scopes:
// Get orderer for items within the same category $orderer = $item->ordering(['category_id' => $item->category_id]); // Reorder only items with category_id = 1 MenuItem::reorderWithinScope(['category_id' => 1]); // Handle null values in scope MenuItem::reorderWithinScope(['parent_id' => null]);
6. Controller implementation for drag-and-drop
Here's how to implement a controller action to handle drag-and-drop reordering from a frontend component:
use Illuminate\Http\Request; use Illuminate\Validation\Rule; class MenuItemController extends Controller { public function updateOrder(Request $request) { $validated = $request->validate([ 'position' => ['required', Rule::in(['before', 'after'])], 'source_id' => ['required', 'integer', 'exists:menu_items,id'], 'target_id' => ['required', 'integer', 'exists:menu_items,id'], ]); $source = MenuItem::findOrFail($validated['source_id']); $target = MenuItem::findOrFail($validated['target_id']); $position = $validated['position']; // Optional: Add scope validation if needed // if ($source->category_id !== $target->category_id) { // return response()->json(['error' => 'Items must be in same category'], 422); // } // Update the source item's order based on target position $source->update([ 'order' => $target->ordering()->$position() ]); return response()->json(['success' => true]); } }
Frontend JavaScript example
// When drag-and-drop completes, send the update request function updateItemOrder(sourceId, targetId, position) { fetch('/menu-items/update-order', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content }, body: JSON.stringify({ source_id: sourceId, target_id: targetId, position: position // 'before' or 'after' }) }) .then(response => response.json()) .then(data => { if (data.success) { console.log('Order updated successfully'); } }); }
With scoped ordering
If you need to maintain ordering within a specific scope (e.g., same category):
public function updateOrder(Request $request) { $validated = $request->validate([ 'position' => ['required', Rule::in(['before', 'after'])], 'source_id' => ['required', 'integer', 'exists:menu_items,id'], 'target_id' => ['required', 'integer', 'exists:menu_items,id'], ]); $source = MenuItem::findOrFail($validated['source_id']); $target = MenuItem::findOrFail($validated['target_id']); $position = $validated['position']; // Ensure items are in the same scope if ($source->category_id !== $target->category_id) { return response()->json(['error' => 'Items must be in same category'], 422); } // Use scoped ordering $scopeAttributes = ['category_id' => $target->category_id]; $source->update([ 'order' => $target->ordering($scopeAttributes)->$position() ]); return response()->json(['success' => true]); }
Testing
Run the test suite:
composer test
Run tests with coverage:
composer test-coverage
Code Quality
Format code with Laravel Pint:
composer format
Run static analysis with Larastan:
composer analyse
Refactor code with Rector:
composer refactor
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.