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.
Architecture fit. kyslik/column-sortable is a thin, opinionated convenience layer over Eloquent's query builder. It ships three things: a Sortable trait that adds a sortable() scope to models, a @sortablelink Blade directive, and a small config file. It does not introduce a service contract, repository abstraction, or middleware — it composes with paginate(), withCount(), and select() rather than replacing them. For a Blade-rendered Laravel monolith or admin panel, this is a near-perfect architectural fit. For an API-first or Inertia/Livewire front end where sorting is driven by client-side table state, the Blade directive (roughly half the package's value) is dead weight and you are effectively adopting a trait for building ORDER BY clauses.
Integration feasibility. Low effort in the happy path: composer require kyslik/column-sortable:^8.0, auto-discovery registers the provider, publish config, add use Sortable plus a $sortable array to the model, call ->sortable() in the controller. Advanced needs — relation sorting via dot notation (detail.phone_number), aliasing via $sortableAs, and join overrides via a {column}Sortable($query, $direction) method — are documented with working examples and a separate demo repository. A single developer can pilot this on one index page in under a day.
Technical risk. The dominant risk is not code quality but stewardship and documentation drift. Release notes show version 8.0.0 adding "Laravel 13.x Compatibility" contributed by the laravel-shift bot, while the README still advertises "Laravel 5.5-8" and the package description says "Laravel 6.x" — three conflicting compatibility statements in the same artifact set. Version history is a sequence of bot/bot-adjacent compatibility PRs (Laravel 9, 10, 11, 13), which means framework support is maintained reactively rather than designed. The reported zero dependents, against 644 stars, is a signal worth resolving before relying on the package: either the metric is a measurement artifact or real-world adoption is thinner than the star count implies. Security posture is reasonable — the $sortable whitelist is the injection boundary — but the fallback path to Schema::hasColumn() when $sortable is undefined inverts that: it queries the schema and infers allowed columns, which is both a per-request DB hit and a weaker contract. There is no multi-column sort, no stable sort tiebreaker, and relation sorting explicitly does not support combining two different relations in one request.
Key questions.
laravel/framework constraint in CI before committing.withCount) sorting? The latter materially increases integration and test scope.ColumnSortableException handling (codes 0/1/2) become a security requirement, not a nicety.sortable() scope with a whitelist is ~50 lines; the package's value is the Blade directive and relation conventions.Stack fit. Target the 8.0.0 line only if we are on a currently supported Laravel (11/12/13); on Laravel 10 pin ^6.5, on 9 pin ^6.4.2. Pin explicitly — the major version has tracked Laravel majors historically, so an unpinned constraint is how a routine composer update becomes an unplanned framework bump. No PHP extension or infrastructure dependency; the only external coupling is Font Awesome, and only for the default icon classes in the Blade directive.
Migration path. Adopt incrementally, per resource, not as a sweep. Sequence: (1) publish config/columnsortable.php and review it against our design system — replace the FA4/FA5 suffix and class defaults with our own icon classes, since shipping Font Awesome assumptions in backend config is a presentation leak; (2) add Sortable and an explicit $sortable array to one high-traffic read model, deliberately never relying on Schema::hasColumn(); (3) wire ->sortable() into that controller and @sortablelink into its Blade partial; (4) add ColumnSortableException handling at the controller boundary; (5) expand to remaining index pages. Keep $sortable arrays next to the model, not derived, so the whitelist is reviewable in a diff.
Compatibility. Verify in CI against our locked Laravel and PHP versions, and against MariaDB and PostgreSQL if we run both — relation sorting and withCount aliasing exercise orderBy on joined and aliased columns, which is exactly where engines diverge. Confirm interaction with paginate() links (the README's appends(\Request::except('page')) pattern is required, not optional — omitting it silently drops the sort across pages). If we already use a data-table library or a query-filter package (e.g. Spatie Query Builder), pick one per resource; running both on the same model creates two competing orderBy authorities.
Sequencing. Config and icon theming first, then one pilot model with relation-free sorting, then relation/alias patterns only if a concrete screen needs them. Defer the {column}Sortable override mechanism until a real join is required; it is the least-tested and most app-specific surface. Budget a short spike to confirm the package's stated framework support against our matrix before the pilot, so we do not discover drift mid-migration.
Maintenance. Low ongoing cost, high bus-factor exposure. The package is small, MIT, and stable in behavior; but support for new Laravel majors has arrived via automated contributor PRs, so our framework upgrade cadence becomes dependent on a third party's bot running on time. Mitigation: treat the package as vendorable. Because the surface we actually use (whitelist + scope + directive) is small, we should be willing and able to fork or inline it during a framework upgrade rather than being blocked. Track the upstream repo's release activity as an explicit line item in our Laravel upgrade checklist.
Support. Community-only, GitHub issues, no vendor SLA. Documentation is good in the README and configuration file, with a linked example repository, but it is drifting relative to releases — the compatibility header is the clearest example. Plan for our own internal one-pager: our pinned version, our config customizations, our whitelist convention, and our exception-handling pattern. That document is the actual support contract.
Scaling. Sorting happens in the database, so application-tier scale is unaffected. Watch three things: (1) the Schema::hasColumn() fallback fires an extra schema query per request for any model lacking $sortable — enforce explicit whitelists and, better, add a lint or code-review check; (2) sortable columns must be indexed, since ORDER BY on an unindexed column with LIMIT/OFFSET pagination degrades badly on large tables; (3) relation sorting generates joins at query time, so validate query plans for the specific screens that use it. Deep-offset pagination is the real ceiling, and it is a pagination problem, not a package problem.
Failure modes. (a) Unhandled ColumnSortableException — the README explicitly warns that a crafted sort parameter (e.g. detail..phone_number) throws code 0; without a controller-level catch this is a user-triggerable 500 and a cheap denial-of-service. (b) Invalid relation — code 1/2 when the relation name is wrong or is not a hasOne, which surfaces as an error page rather than a graceful fallback. (c) Combining two different relations in one request, which the README flags as producing "relation is not defined" errors. (d) Implicit schema-derived whitelisting allowing a sort on a column we never intended to expose. (e) Sort state lost across pagination if appends() is omitted. (f) Silent behavior change on a framework upgrade if the constraint is left loose.
Ramp-up. Very shallow for anyone fluent in Eloquent — the mental model is "a scope plus a directive," learnable in an afternoon from the README. The real ramp-up cost is convention, not API: teams must consistently declare $sortable, catch the exception, and theme the icons. Codify those three in the starter template or a shared Blade component so new index pages inherit them rather than rediscovering them per screen.
How can I help you explore Laravel packages today?