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

Filtereasy Laravel Package

gsferro/filtereasy

FilterEasy is a Laravel package that brings simple, reusable filtering to Eloquent queries. Define allowed filters, map request parameters, and compose query scopes without boilerplate, keeping controllers clean and database queries readable and easy to maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

FilterEasy is a small, single-purpose package: it maps request query-string parameters onto Eloquent query constraints so you don't hand-write when($request->has(...)) chains in every controller. Treat it as a thin convenience layer, not a query builder replacement.

Minimal setup:

composer require gsferro/filtereasy

Then, in the model you want to expose for filtering, pull in the package's trait (the README names the exact trait and configuration array — confirm it against the installed version, since the public API surface is small and has changed across 2024 releases):

use Gsferro\FilterEasy\Traits\Filterable;

class Invoice extends Model
{
    use Filterable;

    protected array $filterable = ['status', 'customer_id', 'created_at'];
}

And in the controller:

public function index(Request $request)
{
    return Invoice::query()
        ->filter($request)      // or ->filter($request->all()) depending on version
        ->paginate(25);
}

Where to look first:

  1. config/filtereasy.php — if the package ships one, publish it with php artisan vendor:publish --tag=filtereasy-config and read it before anything else. It usually controls the query-parameter namespace (e.g. ?filter[status]=paid vs. flat ?status=paid), the delimiter for range/operator syntax, and whether unknown keys throw or are silently ignored.
  2. The trait source in vendor/gsferro/filtereasy/src/ — it's a handful of files. Read the trait method and the filter-resolution loop once; the entire behaviour of the package lives there.
  3. The Filter base class, if one exists, which is where you'd override per-field matching logic.

First real use case: an admin index screen with a search box and two dropdowns. Instead of three conditional ->when() calls, you allowlist status, customer_id, and q, and let the trait handle presence checks and empty-string normalisation. That is the 80% case and the one worth validating on day one.

Implementation Patterns

Allowlist everything, always. The core value of the package is that filtering is opt-in per model ($filterable). Never derive that array from user input, and never let a global/default filter set apply across all models as a shortcut. If the package supports a "fillable-style" wildcard, resist it — an unfiltered column is an unbounded WHERE on an unindexed field.

Keep filter definitions next to the model, not the controller. The package's natural grain is a declarative array on the model. If you need logic (e.g. status=overdue means where('due_at','<',now())), prefer a dedicated filter class or a local scope that the filter array points at, rather than sprinkling closures into controllers:

protected array $filterable = [
    'status'      => 'exact',
    'customer_id' => 'exact',
    'q'           => 'like',        // expands to name/email/reference
    'created_at'  => 'date_range',
];

Normalise the request before it reaches the trait. Build a small FormRequest or a FilterRequest that validates keys and value shapes (status in an enum, customer_id integer, dates parseable). The package will happily apply where('total', '>=', 'abc') on some drivers and return zero rows without an error — validation is your job, not the package's.

Compose with pagination and sorting, but keep them separate. Filtering is a WHERE concern; ordering is a ORDER BY concern. Don't try to express sort=-created_at through the filter array unless the package explicitly supports it; a 6-line sort block in the controller is clearer and less surprising than overloading the filter trait.

Use one filter set per resource, not per route. Defining $filterable as a broad superset and then narrowing per-endpoint with ->filter($request, ['status']) (if supported) keeps the model honest and avoids duplicating arrays between index and an export endpoint that should behave identically.

Reuse for exports and API resources. The strongest day-to-day win is that GET /invoices, GET /api/invoices, and GET /invoices/export.csv all filter identically because they all call the same ->filter($request). Wire the export job to the same code path and you eliminate the classic "the CSV has different rows than the screen" bug.

Test at the HTTP layer. A data-provider test hitting the endpoint with a dozen query-string permutations is worth more than unit-testing the trait: it covers validation, casting, and the trait together.

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

    $this->getJson('/invoices?status=paid')
         ->assertJsonCount(1, 'data');
});

Gotchas and Tips

Treat this package as unmaintained until proven otherwise. One star, zero dependents, a single release in October 2024, and no meaningful community surface mean no security review and no guarantee of Laravel 12 compatibility. Pin the version in composer.json, and wrap its usage behind your own trait or a Filterable interface on your base model so that swapping in tucker-eric/eloquentfilter or a hand-rolled scopeFilter is a one-file change rather than a find-and-replace across every controller.

Verify the calling convention before writing any code. The invocation style (->filter($request) vs ->filter($request->query()) vs a Filter object) and the trait namespace are the two things most likely to differ from what you expect. Read the trait's filter method signature first; a mismatch here produces a BadMethodCallException that looks like a missing trait.

Empty strings are not null. ?status= arrives as '', which many filter implementations will happily turn into where('status', '') and silently return zero rows. Guard in the FormRequest with ConvertEmptyStringsToNull or an explicit nullable rule, and confirm the package's filled() vs has() semantics by testing ?status= explicitly.

Ambiguous columns on joins. If you filter a query that joins customers, an unqualified where('name', ...) throws Column 'name' in where clause is ambiguous. Either qualify columns in the filter definition (invoices.name) or add a table-prefix option if the config offers one. This bites the first time you add a relationship to an index query, not on day one.

LIKE will not use an index. A q filter mapped to %term% full-scan the table. On tables past a few hundred thousand rows, switch to a prefix match (term%), a fulltext index, or Scout. The package gives you no pagination-aware guard here — it will scan the whole set before LIMIT applies.

Cache the config, and clear it when you edit it. If the package ships a config file, php artisan config:cache in production means changes to config/filtereasy.php are invisible until you re-cache. The classic symptom is "works locally, ignored in staging."

Extension point: the scope, not the package. The cleanest customisation is to leave the trait's array to simple equality filters and route anything smarter — overdue, has_refund, multi-column search — through local scopes on the model. If the package allows mapping a key to a method name, use that; if not, a two-line match() in your own scopeFilter wrapper is more debuggable than patching vendor code. Never edit vendor/.

Debug by dumping the SQL. When a filter returns nothing, ->toSql() plus ->getBindings() after ->filter($request) tells you instantly whether the parameter was dropped (presence check failing), mis-cast (string vs int), or applied to the wrong column. Add DB::listen() locally if you want to see it without touching the controller.

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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings