alik-laravel/ordering

Laravel package for ordering functionality

1.0.0 2025-09-17 13:24 UTC

This package is not auto-updated.

Last update: 2025-09-18 11:39:28 UTC


README

Latest Version on Packagist Tests Total Downloads

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.