Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Utils.common.sortable Interface Laravel Package

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.

View on GitHub
Deep Wiki
Context7

Getting Started

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:

  1. vendor/mouf/utils.common.sortable-interface/src/Mouf/Utils/Common/SortableInterface.php — the entire package. Read it before anything else (it's ~20 lines).
  2. 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.
  3. The README on GitHub — it exists mainly to restate the sentence in the package description.

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.

Implementation Patterns

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.

Gotchas and Tips

  • You cannot add parameter type declarations in your implementation. The interface declares 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.
  • Adding a return type is allowed (covariance with implicit 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.
  • Name collision with 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.
  • Direction is a string you must normalise. There's no enum and, depending on the version, maybe no usable constants. Accept asc/ASC/ascending and default to asc; never pass the raw query param through.
  • Column names are an injection vector. orderBy($request->sort) is raw SQL. Whitelist. This is the single most common way sortable interfaces get abused in Laravel apps.
  • Don't rely on 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.
  • The package is effectively unmaintained (last tag 2017, 1 star, zero dependents). That's acceptable for a 20-line interface but means: no PHP 8.4 fixes, no CI, no security process. If 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.
  • No Laravel integration whatsoever. No facade, no config, no auto-discovery. If you expected Sortable::make($query), that's spatie/laravel-query-builder, not this.
  • Watch for sort stability and collation. 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.
  • Extension point is the decorator, not inheritance. The interface has no default behaviour to inherit. If you need fallbacks, logging, caching, or multi-key composition, wrap — don't create a BaseSorter abstract class, because you'll immediately hit single-inheritance limits when a sorter also needs to extend something else.
  • Verdict for a Laravel team: usable, harmless, but only worth it if you have several heterogeneous sortable things (queries, arrays, external API results) that you genuinely want behind one injected abstraction. For a single sortable table, skip the dependency and use orderBy with a whitelist.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums