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

Sortable Columns Laravel Package

zikula/sortable-columns

zikula/sortable-columns adds reusable sortable column support to Zikula tables, helping developers define sortable headers, generate sort links, and keep column ordering consistent across modules with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

zikula/sortable-columns is a small, framework-agnostic helper extracted from the Zikula CMS. It does one job: given the current request and a list of columns, it tells you which column is being sorted, in which direction, and gives you a header object that knows how to build a toggle link. There is no service provider, no config file, no facade, and no Blade integration — you wire it up by hand.

composer require zikula/sortable-columns

Before you commit to it: the repository is archived (last release January 2020) with zero dependents and a single star. Treat it as vendored source code, not as a maintained dependency. If your PHP constraint is 8.1+ and the package's composer.json still says ^7.1, either fork it into app/Support or read it once and reimplement the ~60 lines of logic yourself (a sketch is at the end of this section).

Where to look first, in order:

  1. src/SortableColumns.php — the whole public API lives here: addColumn(), setDefaultSort(), setOrderByFromRequest(), getSort(), getSortDirection().
  2. src/Column.php and src/SortableColumnHeader.php — the value objects, including whatever it exposes for URL/title/CSS state.
  3. README.md and tests/ — the tests are the real documentation; the README assumes you already live inside Zikula.
  4. composer.json — check the PHP constraint and the Symfony http-foundation/routing requirements against your Laravel version.

Your first use case will be an admin index page: GET /admin/users?sort=email&direction=desc, with clickable <th> links.

The pragmatic integration is to use the package only for state (getSort(), getSortDirection()) and let Laravel build the URLs:

use Zikula\Component\SortableColumns\SortableColumns;

$sorter = new SortableColumns(request());
$sorter->addColumn('name', 'Name');
$sorter->addColumn('email', 'Email');
$sorter->addColumn('created_at', 'Joined');
$sorter->setDefaultSort('name');
$sorter->setOrderByFromRequest(request()); // confirm the exact name in src/

$column = $sorter->getSort();          // e.g. 'email'
$dir    = $sorter->getSortDirection(); // 'ASC' | 'DESC'

Laravel's Illuminate\Http\Request extends Symfony\Component\HttpFoundation\Request, so a Symfony Request type-hint will pass. A UrlGeneratorInterface type-hint will not — Laravel's UrlGenerator does not implement it. Check the constructor signature before you get excited.

If that mismatch bites, write the 60 lines yourself:

final class TableSort
{
    public function __construct(
        public readonly string $column,
        public readonly string $direction,
    ) {}

    public static function fromRequest(Request $request, array $allowed, string $default, string $defaultDir = 'asc'): self
    {
        $column = in_array($request->query('sort'), $allowed, true) ? $request->query('sort') : $default;
        $dir = strtolower($request->query('direction', $defaultDir)) === 'desc' ? 'desc' : 'asc';
        return new self($column, $dir);
    }

    public function toggleUrl(string $column): string
    {
        $next = $this->column === $column && $this->direction === 'asc' ? 'desc' : 'asc';
        return request()->fullUrlWithQuery(['sort' => $column, 'direction' => $next, 'page' => 1]);
    }
}

Implementation Patterns

Whitelist before orderBy. The package hands you a string from the query string. Never pass it to Eloquent directly. Map UI keys to real columns:

$map = ['name' => 'users.name', 'email' => 'users.email', 'created_at' => 'users.created_at'];
$query->orderBy($map[$sort->column] ?? 'users.name', $sort->direction);

Apply the default in the query, not just in the helper. The package's default sort only affects what the header reports; your orderBy still needs to run when no ?sort= is present.

Blade partial or component for the header row. Don't call the sorter inside @foreach in a 400-line view. Pass an array of header DTOs from the controller, or bind a Blade::component('sortable-th'):

@foreach ($headers as $key => $label)
    @php $active = $sort->column === $key; @endphp
    <th>
        <a href="{{ $sort->toggleUrl($key) }}" class="{{ $active ? 'font-bold' : '' }}">
            {{ $label }}
            @if ($active) {{ $sort->direction === 'asc' ? '▲' : '▼' }} @endif
        </a>
    </th>
@endforeach

Preserve filters. fullUrlWithQuery() keeps existing query params; a hand-rolled ?sort=...&direction=... will silently drop your search and filter state, which is the single most common bug in this feature.

Reset page to 1 on sort. Sorting page 4 of a differently-ordered list is confusing; include page => 1 in the toggle URL.

Keep pagination links in sync. ->paginate(25)->appends(request()->query()) so page 2 keeps the sort.

Multiple tables per page. sort/direction collide. Support a prefix (users_sort, users_direction) or scope by route.

Reusable wrapper. Wrap the whole thing in a small App\Support\TableSorter service so controllers, API resources, and Inertia props all consume one shape ({column, direction, headers: [{key, label, url, active, direction}]}). That wrapper is also the seam that lets you swap the package out later without touching views.

For JSON/Inertia endpoints, expose the sort state in the resource meta and let the frontend render arrows — the package contributes nothing beyond the two strings.

Testing. Feature-test the query string, not the markup: $this->get('/admin/users?sort=email&direction=desc')->assertOk(), plus a unit test on the whitelist fallback for ?sort=id;DROP TABLE.

Gotchas and Tips

  • Archived means archived. No PHP 8.3 fixes, no Symfony 7 compatibility, no security response. Pin the exact version, or fork to app/Support/SortableColumns and take ownership. For a helper this small, forking is usually cheaper than an adapter layer.
  • Zikula assumptions leak. The code was written against Symfony routing and Zikula's controller conventions. If you hit a UrlGeneratorInterface or AbstractController requirement, don't fight it — extract getSort()/getSortDirection() and abandon the rest.
  • Direction casing. The package deals in ASC/DESC; Laravel's orderBy accepts either but request()->query('direction') from a hand-written link may be lowercase. Normalize at the boundary.
  • No escaping. Titles and URLs are strings out of the object — always render through {{ }} in Blade. Don't {!! !!} a column label you built from user input.
  • URL params get dropped on toggle. Verify against your filters; this is the #1 thing to test. Adding a search box later is exactly when it breaks.
  • Don't render the header inside a loop that re-instantiates the sorter. Construct once per request, pass the state down.
  • Extension points are thin: subclass SortableColumnHeader (or wrap it) if you need to attach Tailwind/Bootstrap classes, aria-sort, and rel="nofollow". Wrapping in your own DTO is cleaner than subclassing an unmaintained class.
  • SEO/caching. Sort links produce duplicate content and cache-key explosion. Add rel="nofollow", and if you cache index pages, include sort params in the cache key.
  • Debugging tip: dump getSort()/getSortDirection() at the top of the controller with no ?sort present. If you get null/ASC instead of your default, you've called setOrderByFromRequest() before setDefaultSort() — order matters.
  • Consider the alternative. spatie/laravel-query-builder (allowedSorts(), sort(), ->appends()) is maintained, Laravel-native, and covers this plus filtering and includes. For a five-column admin table, either is fine; for anything load-bearing, pick the maintained one. Use zikula/sortable-columns only if you're already inside a Zikula codebase or the license/behavior is a hard requirement.
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.
robertmarney/lara-hierarchial-collections
simonschaufi/rule-doc-generator
emissaoja/emissaoja
blackbricksoftware/gravityforms-civicrm-form-processor
al-rahabi/fancy-crud
jinas/dhivatar-component
mltstephane/laravel-quality
bernskiold/laravel-talentlms
bernskioldmedia/laravel-form-architect
youbar/easy-crud
c-point-labs/laravel-hostyourai
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo