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

Column Sortable Laravel Package

kyslik/column-sortable

kyslik/column-sortable adds easy column sorting to Laravel 5.5–8 apps. Use Blade helpers to generate sortable links, configure sortable columns, and sort by related hasOne/belongsTo fields, with override and alias options for custom queries.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the version that matches your Laravel release — they are not interchangeable.

# Laravel 9  -> ^6.4
# Laravel 10 -> ^6.5
# Laravel 11 -> ^6.6
# Laravel 12 -> ^7.0
# Laravel 13 -> ^8.0
composer require kyslik/column-sortable

Auto-discovery registers ColumnSortableServiceProvider. Publish the config once, because that file is where 90% of the customisation lives:

php artisan vendor:publish --provider="Kyslik\ColumnSortable\ColumnSortableServiceProvider" --tag="config"

Then add the trait and, critically, the whitelist:

class User extends Model
{
    use Sortable;

    public $sortable = ['id', 'name', 'email', 'created_at', 'updated_at'];
}

Controller and view are two lines:

$users = User::sortable()->paginate(10);
@sortablelink('name', 'Name')
...
{!! $users->appends(\Request::except('page'))->render() !!}

First use case to build: an admin index page. It is the lowest-risk place to prove the pattern (whitelisted columns, no user-generated SQL surface beyond asc/desc) before you spread it to customer-facing tables.

Where to look first: config/columnsortable.php (icon classes, default direction, relation separator), then the Sortable trait source to confirm the scope signature and the xxxSortable override convention. The README's "Full Example" repo is more informative than the prose.

Implementation Patterns

Default sort for the empty-URL case. The first page load has no ?sort=, so the table appears unordered. Bake a default into the controller:

$users = User::sortable(['name' => 'desc'])->paginate(10);
// or simply: ->sortable('name')   // direction comes from config

Whitelist plus relation sorting. Relation sorting is the feature that justifies the package. Define the relation on the model, sort with dot notation, and eager-load it:

public function detail() { return $this->hasOne(UserDetail::class); }

// controller
$users = User::with('detail')->sortable()->paginate(10);
@sortablelink('detail.phone_number', 'Phone')

The $sortable arrays must be declared on both models, otherwise the package falls back to Schema::hasColumn() and issues an extra query per request.

Override methods for anything the package can't express. The convention is {column}Sortable($query, $direction). This is your escape hatch for joins, aggregates, and CASE expressions:

public function addressSortable($query, $direction)
{
    return $query->join('user_details', 'users.id', '=', 'user_details.user_id')
                 ->orderBy('address', $direction)
                 ->select('users.*');
}

Aliasing for computed columns. $sortableAs bypasses the column-exists check and suppresses table prefixing, which makes withCount() and select(... as ...) sortable:

public $sortableAs = ['orders_count'];

$users = User::withCount('orders')->sortable(['orders_count'])->paginate(10);

Preserve filters across sort clicks. The third @sortablelink argument appends GET parameters, so a filtered table doesn't lose its filters when the user re-sorts:

@sortablelink('name', 'Name', ['status' => request('status')])

Better still, wrap the directive in your own Blade component/partial so every table header in the app gets identical behaviour, icon classes, and query preservation without copy-paste.

Keep it testable. The sort is pure query-string input, so a feature test asserting the order of returned IDs is cheap and catches regressions when you refactor overrides:

$this->get('/users?sort=name&direction=desc')->assertSeeInOrder([...]);

Gotchas and Tips

  • appends(Request::except('page')) is mandatory. Without it, page 2 of a sorted table silently reverts to the default order. This is the single most common "it's broken" report.
  • The package prefixes columns with the table name (users.name). In joined queries this becomes ambiguous and throws. Fix with $sortableAs or an override method — don't try to out-guess the prefixing.
  • Always declare $sortable. Omitting it triggers Schema::hasColumn() on every request, which is an extra round-trip you'll notice on wide tables.
  • Wrap calls in a try/catch for ColumnSortableException. The sort parameter is user input; code 0 is a malformed value (sort=detail..phone), code 1 an unknown relation, code 2 a relation that isn't hasOne. Catch it and fall back to the default order rather than showing a 500.
  • Only one relation at a time. Mixing detail.phone_number and user.name in the same request produces relation-not-defined errors. If you need multi-column sort, implement it yourself.
  • Self-referencing models get prefixed parent_ (comments, categories), which changes the column name you pass to @sortablelink. Check issue #60 before debugging blind.
  • Font Awesome classes default to FA4 (-asc/-desc). On FA5+ set asc_suffix => -up and desc_suffix => -down. Also register your columns under numeric/amount/alpha in the config, or the icon renders but never changes.
  • Blade directive needs whitespace before it: <tr> @sortablelink('Name') — not <tr>@sortablelink(...).
  • Inertia/Livewire/API clients can't use the Blade directive. Reimplement the link with request()->fullUrlWithQuery(['sort' => $col, 'direction' => $next]); the server-side scope still works unchanged.
  • Cache keys must include the full query string. A sorted listing and an unsorted one are different responses; request()->fullUrl() is the safe key.
  • Extension points are limited to the config file and override methods. There is no custom-driver hook; if you need multi-column sorting or per-user sort preferences persisted server-side, build it around the package rather than fighting it.
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.
adachsoft/changelog-linter
adachsoft/collection
payerurl/telegrambot-crypto-payment-instant-payout
dskripchenko/laravel-hybrid-search
riorizkyrainey/lara-ced
mehedimi/static-vel
zharp/zharp
nonsapiens/bigquery-model-sync
lgu-bayambang-ict-office/lgu-laravel-development-standards
andydefer/laravel-support
se7enxweb/flysystem
lampwick/laravel
jeslage/statamic-emojis
micahdshackelford/rmap-laravel
draftsmaninc/draftsman
square-bit/laravel-volition
himito/laravel-mailman
htinlynn/genius-helper
maco-studios/hephaestus
adachsoft/php-code-style