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

Query String Filter Laravel Package

smorken/query-string-filter

A Laravel package for filtering Eloquent model queries directly from URL query string parameters. Define allowed filters, keep controllers clean, and build flexible, request-driven search and listing endpoints with minimal configuration.

View on GitHub
Deep Wiki
Context7

Getting Started

Install

composer require smorken/query-string-filter

There's no service provider or config to publish in most versions of this package — it ships a trait you attach directly to your Eloquent models.

First look

  1. Open vendor/smorken/query-string-filter/src/ — the whole package is usually one or two files. Read the trait top to bottom before wiring it in; it's small enough that guessing about its behaviour costs more than reading it.
  2. Note the method name used for filtering (typically filter() on the Eloquent builder, or a scopeFilter on the model). This is your entry point.
  3. Check which query-string keys it maps by convention — usually the key is the name of a scope on the model (?status=active calls scopeStatus).

Minimal working setup

use Smorken\QueryStringFilter\Traits\HasFilters; // confirm namespace from source

class Post extends Model
{
    use HasFilters;

    public function scopeStatus($query, $value)
    {
        return $query->where('status', $value);
    }

    public function scopeAuthor($query, $value)
    {
        return $query->whereHas('author', fn ($q) => $q->where('name', $value));
    }
}
// routes/web.php
Route::get('/posts', fn () => Post::filter(request()->query())->paginate(25));

Request GET /posts?status=published&author=scott now applies both scopes. That scope-per-querystring-key convention is the core mental model — everything else is a variation on it.

First real use case: a list/index endpoint where you want ?status=, ?sort=, ?q= handled without writing a $request->when() chain per controller action.

Implementation Patterns

Whitelist on the model, not the controller. The safest pattern is to define scopes only for the parameters you actually want public. Because the trait maps keys to scope names, an un-whitelisted key simply resolves to no method and is dropped. Keep sensitive relationships (e.g. scopePassword) out of scope-name space entirely — don't rely on controllers to filter keys.

Scope-per-filter is the idiomatic extension point.

public function scopeCreatedAfter($query, $date)
{
    return $query->whereDate('created_at', '>=', $date);
}

public function scopeSearch($query, $term)
{
    return $query->where(function ($q) use ($term) {
        $q->where('title', 'like', "%{$term}%")
          ->orWhere('body', 'like', "%{$term}%");
    });
}

Keep scopes single-purpose and composable; combine filtering, sorting, and pagination in the controller rather than one mega-scope.

Sort helper pattern. If the package exposes a sortable mechanism (check for a scopeSort or a sort config), implement it explicitly against an allow-list of columns to avoid interpolating a user-supplied column name:

public function scopeSort($query, $value)
{
    $allowed = ['title', 'created_at', 'status'];
    $direction = str_starts_with($value, '-') ? 'desc' : 'asc';
    $column = ltrim($value, '-');

    return in_array($column, $allowed, true)
        ? $query->orderBy($column, $direction)
        : $query; // silently ignore unknown columns
}

Compose with pagination and eager loading.

$posts = Post::with('author')
    ->filter($request->query())
    ->paginate($request->integer('per_page', 25))
    ->withQueryString(); // preserve filters across pages

withQueryString() is important — Laravel pagination links drop query params otherwise, which breaks multi-page filtered lists.

Reusable base pattern for large APIs. Extract the filter call into a per-resource filter class or a repository method rather than repeating Post::filter(...) across controllers. Because the package is convention-based, consistency matters more than abstraction:

abstract class IndexController
{
    protected function index($query, Request $request)
    {
        return $query->filter($request->query())->paginate();
    }
}

Integration with API resources / form requests. Validate the query string before it reaches the trait. A FormRequest with rules like ['status' => 'in:draft,published', 'sort' => 'string', 'per_page' => 'integer|max:100'] catches bad input early and gives you 422s instead of silently-ignored filters, which is easier to debug.

Caching. Build cache keys from the normalized query string array (ksort($filters) then md5(json_encode($filters))) so ?status=a&sort=b and ?sort=b&status=a share a cache entry.

Gotchas and Tips

Unknown filters are silently dropped — that's a feature and a trap. A typo (?stats=active) returns unfiltered results with no error. During debugging, log the resolved filter keys, or temporarily throw in your base scope when !method_exists($this, 'scope'.Str::studly($key)) while developing.

Everything arrives as a string. ?published=0 passes "0", which is truthy in PHP. Cast inside your scope ((bool) $value, (int) $value) rather than trusting the caller.

Empty values. ?status= yields an empty string, not null. Decide whether empty means "ignore" or "filter to nothing" and handle it explicitly — most bugs in filtered lists come from an empty search field narrowing results unexpectedly.

Column interpolation in sort scopes is the main injection risk. Never do orderBy($request->sort). Always allow-list, as above. The package does not know your schema; it only dispatches to your scopes.

N+1 on relationship scopes. scopeAuthor using whereHas is fine, but calling ->author in the resource collection afterwards re-queries. Pair relationship filters with with() on the same eager-loaded relationship.

Case sensitivity differs by driver. MySQL is case-insensitive by default, PostgreSQL is not. A like search scope that works locally can behave differently in production; use ilike or lower() on Postgres.

Pagination links lose filters without withQueryString(). Nearly every "filters reset when I go to page 2" issue is this.

Low package activity. With no stars, no dependents, and a Bitbucket-only repo, expect no upstream fixes. Pin the version in composer.json and treat the trait as vendored code you own. If you need more than key-to-scope dispatch (e.g. per-field operator syntax like ?price[gte]=10), you'll write it yourself — consider copying the trait into app/Support/ and dropping the dependency rather than extending a package you can't patch upstream.

Extension points to look for. A $filterable property to allow-list keys, an overridable applyFilter() method, and any config for default sort. If the version you install exposes these, prefer them over reimplementing dispatch — they're the only stable seams in a package this small.

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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource