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

Laravel Simple Query Filter Laravel Package

exeko/laravel-simple-query-filter

A lightweight Laravel package for adding simple, declarative query-string filters to Eloquent models, letting you turn request parameters into reusable filter logic with minimal code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin. This package has had a single release (2020-09-24), zero stars, and zero dependents. Before composer require exeko/laravel-simple-query-filter, check its composer.json constraints against your PHP and Laravel versions — a 2020 trait package was likely written against Laravel 6/7 and PHP 7.2–7.4. If you're on Laravel 10/11 with PHP 8.2+, expect to either fork it or copy the trait into app/Support/ rather than depend on it. Pin an exact version ("exeko/laravel-simple-query-filter": "1.0.0") so a stray tag can't change behavior under you.

Wire it into one model. The package is a trait that turns request query parameters into Eloquent constraints. The first step is always the same: add the trait to a model, then implement one filter method per parameter you want to expose.

use Exeko\SimpleQueryFilter\Filterable;

class Invoice extends Model
{
    use Filterable;
}

Find the convention first. Open the trait source before writing any filters. The important questions to answer in the first five minutes:

  • Is the entry point scopeFilter, filter(), or a static call?
  • Does it look for filterStatus() methods, scopeStatus() scopes, or entries in an $filters array property?
  • Does it read request() directly, or take an array argument (which makes it testable)?

That single detail determines everything downstream, so grep the trait for request(, method_exists, and scope before committing to a pattern.

First use case. A paginated index endpoint with a couple of optional filters is the canonical fit:

// GET /invoices?status=paid&customer_id=42
public function index()
{
    return Invoice::query()
        ->filter()          // or ->filter($request->all()) depending on the trait's signature
        ->paginate(25);
}

Start with exactly one filter and one integration test asserting that ?status=paid narrows the result set and that no parameter returns everything. That test is your guardrail for every later change.

Implementation Patterns

One method per parameter, named deterministically. Whatever the trait's lookup convention is, keep the mapping boring: query param status → filterStatus($value). Avoid clever pluralization or aliasing; the value of this kind of package is that a new developer can guess the method name from the URL.

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

Return $this from each method so the trait can chain, and never call ->get() inside a filter method — filters constrain, they don't execute.

Treat the query string as untrusted input, always. A filter trait that reads request()->all() will happily accept any key. Two defenses, use both:

  1. An explicit allowlist — either the trait's $filters property or a $request->validate() in the controller.
  2. Validation of values, not just keys: 'status' => ['sometimes', Rule::in(['paid', 'pending', 'void'])].

Never let a filter method use a request-supplied value as a column name (orderBy($request->sort), where($request->column, ...)). That is where the injection surface actually lives, and a thin trait will not protect you from it.

Group filters behind a FormRequest for anything non-trivial. Once you have more than three filters, move the validation out of the controller:

class InvoiceIndexRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'status'      => ['sometimes', Rule::in(InvoiceStatus::values())],
            'customer_id' => ['sometimes', 'integer', 'exists:customers,id'],
            'from'        => ['sometimes', 'date'],
            'to'          => ['sometimes', 'date', 'after_or_equal:from'],
        ];
    }
}

Then pass only $request->validated() into the filter call. This is the single highest-value pattern around the package: it converts "whatever the client sent" into a known-shape array, which makes the trait's behavior predictable and the tests readable.

Handle arrays explicitly for multi-value filters. ?status[]=paid&status[]=pending arrives as an array. Decide per filter whether that means whereIn or is invalid:

public function filterStatus($value)
{
    return is_array($value)
        ? $this->whereIn('status', $value)
        : $this->where('status', $value);
}

Relationship filters need whereHas, and the trait won't do it for you. A customer parameter that should match on the customer's name is not a column on invoices:

public function filterCustomer($value)
{
    return $this->whereHas('customer', fn ($q) => $q->where('name', 'like', "%{$value}%"));
}

Watch for the N+1 that follows: eager-load customer in the controller if the response serializes it.

Compose with the rest of the query builder, don't replace it. Filters are just constraints. Keep authorization scopes, soft-delete handling, and eager loads at the call site:

Invoice::query()
    ->whereBelongsTo($request->user()->organization)
    ->with('customer')
    ->filter($request->validated())
    ->latest()
    ->paginate(25);

Extract shared filters into a second trait. If several models filter on created_at ranges or a search term, put those methods in App\Support\Filters\HasDateRange and use it alongside the package trait. This keeps the package doing only what it's good at (dispatching) and puts your domain logic somewhere you own.

Test filters at the model layer, not through HTTP. One test per filter method, plus one HTTP test per endpoint to prove wiring:

it('filters by status', function () {
    Invoice::factory()->create(['status' => 'paid']);
    Invoice::factory()->create(['status' => 'pending']);

    expect(Invoice::filter(['status' => 'paid'])->count())->toBe(1);
});

This only works if the trait accepts an array; if it reads request() internally, wrap each test in $this->get(...) or refactor the entry point to take the array (see Gotchas).

Gotchas and Tips

The maintenance risk is the headline. No release since September 2020, no dependents, no stars means no community pressure to fix a bug you find. Concretely: the trait may use request() in a way that breaks under Laravel's newer request lifecycle, may reference str_*/Arr::* helpers that moved, or may not declare PHP 8 return types correctly. Verify it under your actual framework version before it reaches main. If it works, consider copying the ~50-line trait into your app/ directory and dropping the dependency — you lose Composer updates you weren't getting anyway and gain the ability to fix things.

Unrecognized parameters fail silently or loudly depending on the trait. Work out which. Silent ignore means a typo (?statuss=paid) returns the full dataset and nobody notices until a support ticket. Loud failure (throwing on a method that doesn't exist) is actually safer, but it means any bot probing your URL can 500 your endpoint — which is an argument for validating the allowlist before the trait sees the input.

Empty strings are not nulls. ?status= sends an empty string, not a missing key. A naive filterStatus($value) becomes where status = '' and returns nothing. Guard the common case:

public function filterStatus($value)
{
    return $value === null || $value === ''
        ? $this
        : $this->where('status', $value);
}

Decide the policy once (empty means "no filter") and apply it consistently.

Watch for trait collisions. If you're also using another filtering package, a HasFilters trait, or Laravel's scopeFilter convention, two traits defining filter/scopeFilter will produce a fatal error or, worse, one silently wins. Check class_uses on the model and keep exactly one filtering entry point per model.

like filters are case-sensitive on PostgreSQL and case-insensitive on MySQL. where('name', 'like', "%{$v}%") behaves differently across your dev and production databases. Use whereRaw('LOWER(name) LIKE ?', ['%'.strtolower($v).'%']) or ilike on Postgres, and add an index if the searched columns are large — a leading-wildcard like cannot use a B-tree index.

Pagination parameters are not filters. ?page=2&per_page=50 will be passed to the trait like everything else. If the trait doesn't skip them, you'll get a fatal error or an accidental constraint. Either exclude them explicitly in the allowlist or use $request->except(['page', 'per_page']).

Debugging: dump the SQL, not the request. When a filtered query returns the wrong rows, the fastest path is ->filter($params)->toSql() plus ->getBindings(). Most surprises are a filter method that never ran (wrong method name convention), an allowlist that rejected the key, or a value type mismatch ("42" vs 42 for an integer column, which is fine on MySQL and fatal on Postgres in some drivers).

Extension points worth knowing. The trait is small, so the useful extension surface is: (a) override the dispatching method in your model to add logging or a default sort; (b) add a filterSearch method that fans out across multiple columns; (c) put per-model defaults in a bootFilterable() if the trait supports it. If none of these are clean, that's your signal to fork — the file is small enough that owning it is cheaper than fighting it.

Add a fallback test that fails loudly. One test asserting an unknown query parameter produces a 422 (or a 200 with unfiltered results, whichever you chose) will catch the day someone upgrades Laravel and the trait's parameter discovery changes. Given the release history, that test is the cheapest insurance you can buy.

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.
gmostafa/php-graphql-client
xeviant/paystack
snowsoft/faker
meditationsteps/blogetc
animalyoung/laravel-influxdb
onix-solutions/sms-telera
lonely-lights/prosetta
oxygen/data
oxygen/core
jengo/base
xiloryx/log-collector
a21ns1g4ts/filament-stripe
la-php/laravel-admin
artisan-build/community-prompts
vleroy/smart
ayles-software/laravel-australian-public-holidays
cboxdk/laravel-id-spatie
cboxdk/laravel-id-billing
cboxdk/laravel-id-billing-bridge
simple-as-fuck/php-performance-log