mouf/utils.common.sortable-interface
A small PHP package from Mouf that defines a common SortableInterface, helping standardize sortable behavior across libraries and components so objects can be sorted consistently in your codebase.
This package is a contract, not a library. It ships one file — an interface with a single untyped method — and nothing else: no service provider, no config, no trait, no concrete implementation. Treat it as "a vocabulary word" you can adopt so that every sortable thing in your app speaks the same API.
composer require mouf/utils.common.sortable-interface
Where to look first, in this order:
vendor/mouf/utils.common.sortable-interface/src/Mouf/Utils/Common/SortableInterface.php — the entire package. Read it before anything else (it's ~20 lines).composer.json inside the vendor dir — confirm the PSR-4 root (Mouf\Utils\Common\) and the PHP constraint. It was last tagged in 2017, so expect php: >=5.3 style constraints and no ^8 declaration.The interface boils down to roughly this shape (verify constants/return docblock against the actual file, they vary between Mouf packages):
namespace Mouf\Utils\Common;
interface SortableInterface
{
const SORT_ASC = 'ASC';
const SORT_DESC = 'DESC';
/**
* @param string $key The column/property to sort on
* @param string $direction ASC or DESC
*/
public function sort($key, $direction);
}
First use case — a sortable admin index. You have GET /admin/users?sort=email&direction=desc and you want the controller to depend on an abstraction rather than reach into the query builder:
namespace App\Sorting;
use Illuminate\Database\Eloquent\Builder;
use Mouf\Utils\Common\SortableInterface;
final class UserSorter implements SortableInterface
{
private const ALLOWED = ['name', 'email', 'created_at'];
public function __construct(private Builder $query) {}
public function sort($key, $direction)
{
$column = in_array($key, self::ALLOWED, true) ? $key : 'created_at';
$direction = strtoupper((string) $direction) === 'DESC' ? 'desc' : 'asc';
return $this->query->orderBy($column, $direction);
}
}
Bind it in a service provider (the builder is request-scoped, so a factory is cleaner than a singleton):
$this->app->bind(UserSorter::class, fn ($app) => new UserSorter(User::query()));
That's the whole onboarding. Everything after this is designing the implementations well, because the package gives you zero help there.
1. The sorter as a query decorator (the primary pattern). Keep the sort() implementation stateless apart from the builder it wraps, and return the builder so it composes with pagination:
$sorter = new UserSorter(User::query());
$users = $sorter->sort($request->query('sort'), $request->query('direction'))
->paginate(25)
->withQueryString();
Sort before paginate()/cursorPaginate(), never after — ordering a paginated result set in PHP only reorders the current page.
2. Collection / in-memory implementation. For exported reports, cached arrays, or Filament-style table data, a second implementation keeps the call site identical:
final class CollectionSorter implements SortableInterface
{
public function __construct(private Collection $items) {}
public function sort($key, $direction)
{
$sorted = $this->items->sortBy($key, SORT_NATURAL | SORT_FLAG_CASE);
return strtoupper((string) $direction) === 'DESC' ? $sorted->reverse()->values() : $sorted->values();
}
}
3. Whitelist as a decorator, not an if inside every sorter. Column names cannot be parameter-bound in SQL, so centralise the guard once and wrap any implementation:
final class AllowedColumnsSorter implements SortableInterface
{
public function __construct(
private SortableInterface $inner,
private array $allowed,
private string $fallback = 'created_at',
) {}
public function sort($key, $direction)
{
$key = in_array($key, $this->allowed, true) ? $key : $this->fallback;
return $this->inner->sort($key, $direction);
}
}
4. Composite / multi-column sorting. Because sort() returns whatever the implementation returns, you can chain for tie-breaking (ORDER BY last_name ASC, first_name ASC):
$sorter->sort('last_name', 'asc')->orderBy('first_name');
If you want a single contract for multi-key requests, write a CompositeSorter that holds [['key' => ..., 'direction' => ...], ...] and delegates in order. This is usually where teams outgrow the interface and reach for spatie/laravel-query-builder.
5. Reusable Blade header partial. The interface is most valuable at the view layer — every table header links through the same URL builder:
@php
$current = request('sort');
$next = $current === $key && request('direction') !== 'desc' ? 'desc' : 'asc';
@endphp
<th>
<a href="{{ request()->fullUrlWithQuery(['sort' => $key, 'direction' => $next]) }}">
{{ $label }}
@if ($current === $key) {{ request('direction') === 'desc' ? '▾' : '▴' }} @endif
</a>
</th>
6. Validate at the edge, not in the sorter. A FormRequest with Rule::in([...]) and Rule::in(['asc','desc']) gives you a 422 for free and keeps the sorter free of defensive branches. Keep the whitelist in one place (a config array or the sorter's constants) and reference it from both.
7. Testing. Because it's a pure contract, tests are trivial: assert the builder's orders array for the Eloquent implementation, and assert element order for the collection one. Use an anonymous class implementing SortableInterface as a spy when testing controllers that consume a sorter.
8. Contextual binding if two resources need different sorters injected into the same abstract type:
$this->app->when(OrderController::class)
->needs(SortableInterface::class)
->give(OrderSorter::class);
That is the one genuinely Laravel-native thing you can do with a bare interface, and it's the reason to depend on the interface rather than the concrete class.
sort($key, $direction) with no types. PHP's contravariance rules mean widening is fine, narrowing is a fatal error — public function sort(string $key, string $direction) will blow up at class-load time. Keep params untyped and normalise inside the method.mixed), but don't lean on it: the docblock in the package may say void while every sane implementation returns $this or a builder. Pick one convention in your codebase and document it, because the package won't.Collection::sort(). Never implement this interface on a class extending Illuminate\Support\Collection (or anything with a sort() already). Collection::sort(callable $callback = null) and sort($key, $direction) are incompatible signatures — fatal error, and a confusing one. Compose a collection inside the sorter instead of subclassing it.asc/ASC/ascending and default to asc; never pass the raw query param through.orderBy($request->sort) is raw SQL. Whitelist. This is the single most common way sortable interfaces get abused in Laravel apps.SORT_ASC/SORT_DESC constants existing. Check the source file; if they're absent, define your own enum/constants in App\Sorting\Direction and translate at the boundary. Referring to a constant that doesn't exist is a fatal error at runtime, not a warning.composer require fights you on platform constraints, composer require mouf/utils.common.sortable-interface --ignore-platform-reqs works, but at that point consider copying the interface into App\Contracts — a one-file contract has no upgrade path worth preserving.Sortable::make($query), that's spatie/laravel-query-builder, not this.ORDER BY on a non-unique column with pagination can duplicate/skip rows across pages; always add a unique tiebreaker (->orderBy('id')) as the final order clause.BaseSorter abstract class, because you'll immediately hit single-inheritance limits when a sorter also needs to extend something else.orderBy with a whitelist.How can I help you explore Laravel packages today?