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

Filter Laravel Package

rmasters/filter

rmasters/filter is a Laravel package for filtering Eloquent models from request query strings. Define reusable filters, map them to parameters, and keep controllers clean with composable, request-driven query filtering.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and wire it up

composer require rmasters/filter

Then check two things before writing any code:

  1. composer.json of the package — look at the illuminate/* version constraints. This package is small and old (21 stars, MIT, predates Laravel 5.5 auto-discovery in all likelihood). If your app is on Laravel 9/10/11 you will probably need --ignore-platform-reqs or a fork; confirm this on day one rather than debugging a confusing "trait not found" later.
  2. src/ — the trait and the service provider. The whole package is likely one trait plus a service provider. Read it end-to-end (10 minutes) so you know exactly how filter names map to methods and how the input array is resolved.

Register the provider manually if you're below 5.5:

// config/app.php
'providers' => [
    // ...
    Rmasters\Filter\FilterServiceProvider::class,
],

First use case: an index listing driven by the query string

The canonical job for this package is turning ?name=ross&active=1&sort=-created_at into a constrained query without hand-writing if ($request->has(...)) blocks in every controller.

// app/Models/User.php
use Rmasters\Filter\Filterable;

class User extends Model
{
    use Filterable;

    // Explicit allow-list — see Gotchas, this matters
    protected $filters = ['name', 'email', 'active', 'created_after'];
}
// app/Http/Controllers/UserController.php
public function index(Request $request)
{
    return User::filter($request->only(['name', 'email', 'active', 'created_after']))
        ->paginate(25);
}

If that runs, you've validated the core contract. Everything else is refinement.

Implementation Patterns

Pattern 1 — Controller as a thin adapter, filter logic on the model

Keep the controller dumb. It should only decide which keys are allowed and pass them through; the model owns the SQL.

public function index(Request $request)
{
    $users = User::filter($request->only($this->allowedFilters()))
        ->with('team')
        ->paginate(25)
        ->appends($request->query());

    return view('users.index', compact('users'));
}

private function allowedFilters(): array
{
    return ['name', 'email', 'active', 'team_id', 'created_after'];
}

appends($request->query()) is the detail people forget — without it your pagination links drop the filters on page 2.

Pattern 2 — One method per filter, named predictably

Regardless of whether the package maps name → filterName() or scopeFilterName(), the discipline is the same: one small, testable method per concept.

public function filterName($query, $value)
{
    return $query->where('name', 'like', "%{$value}%");
}

public function filterActive($query, $value)
{
    return $query->where('active', (bool) $value);
}

public function filterCreatedAfter($query, $value)
{
    return $query->where('created_at', '>=', Carbon::parse($value)->startOfDay());
}

Return the query builder every time. If the package's convention is to mutate $this instead, mirror that — but be consistent across the whole model.

Pattern 3 — Filters that are really "query objects"

For anything beyond where x = y (date ranges, "search across several columns", whereHas), don't cram it into a filter method. Delegate:

public function filterSearch($query, $term)
{
    return $query->where(function ($q) use ($term) {
        $q->where('name', 'like', "%{$term}%")
          ->orWhere('email', 'like', "%{$term}%");
    });
}

public function filterTeam($query, $teamId)
{
    return $query->whereHas('team', fn ($q) => $q->where('id', $teamId));
}

Note the closure around the orWhere — wrap OR groups or a later where will silently change the result set.

Pattern 4 — Sorting alongside filtering

Most filter packages don't do ordering. Handle it explicitly so ?sort=-created_at doesn't leak into the filter input:

$sort = $request->input('sort', '-created_at');
$direction = str_starts_with($sort, '-') ? 'desc' : 'asc';
$column = ltrim($sort, '-');

$users = User::filter($request->only($allowed))
    ->orderBy(in_array($column, ['name', 'created_at', 'email']) ? $column : 'created_at', $direction)
    ->paginate(25);

Never pass a raw user string to orderBy.

Pattern 5 — Compose with existing scopes

Filterable models rarely exist in isolation. Layer scopes for concerns that aren't user-driven:

User::active()                       // scope, always applied
    ->filter($request->only($allowed))
    ->paginate(25);

This keeps the filter input purely about user-supplied refinements and the scope about business rules.

Pattern 6 — Validate before you filter

Filters change the shape of a query, but they're still input. A FormRequest with sometimes|string|max:255 on each filter key costs nothing and turns silent typos into 422s your frontend can render.

class UserIndexRequest extends FormRequest
{
    public function rules(): array
    {
        return [
            'name'         => ['sometimes', 'string', 'max:255'],
            'active'       => ['sometimes', 'boolean'],
            'created_after'=> ['sometimes', 'date'],
        ];
    }
}

Pattern 7 — Test the filters, not the controller

Because the filter lives on the model, you can assert SQL-level behaviour directly:

public function test_name_filter_is_partial_match()
{
    User::factory()->create(['name' => 'Ross Masters']);
    User::factory()->create(['name' => 'Jane Doe']);

    $this->assertCount(1, User::filter(['name' => 'ross'])->get());
}

Gotchas and Tips

The allow-list is not optional. If the package iterates whatever array you hand it and calls a dynamically-named method, passing $request->all() is an invitation. Either pass $request->only([...]) or validate with a strict rule set. Prefer both.

Empty and falsy values are the #1 source of bugs. ?active=0 and ?active= and an absent key are three different things. $request->all() will give you '' for the middle case, (bool) '' is false, and your "show only inactive" filter silently becomes "show all inactive plus everything else." Decide up front whether your filters ignore empty strings:

public function filterName($query, $value)
{
    return $value === '' || $value === null
        ? $query
        : $query->where('name', 'like', "%{$value}%");
}

Or strip empties in the controller with array_filter($input, fn ($v) => $v !== '' && $v !== null). Pick one and apply it everywhere.

Array inputs (?ids[]=1&ids[]=2). where('id', $array) throws. If you accept arrays, convert with whereIn. If you don't, reject them in validation — string rules will do it.

Case sensitivity. MySQL's default collation makes LIKE '%ross%' case-insensitive; Postgres's LIKE is not. If you're on Postgres, use ilike or lower() consistently, or your tests will pass locally and fail in staging.

Version drift. This package targets an older Laravel generation. Things to verify on install:

  • Does the trait assume Model internals that have since changed (e.g. newQuery(), $this->query)?
  • Does the service provider reference Illuminate\Support\ServiceProvider methods that are deprecated?
  • Does it rely on array_get/str_* helpers that were removed?

If it's incompatible, the trait is almost certainly small enough to copy into app/Support/Filterable.php and maintain yourself — often a better trade than pinning your whole framework. Do this deliberately, with a note in your README, so the next developer isn't confused about why composer.json mentions a package that isn't used.

Debugging filters. When a filter produces the wrong result set, dump the SQL rather than reasoning about it:

$query = User::filter($input);
dd($query->toSql(), $query->getBindings());

Also log which filters actually fired — a two-line Log::debug inside a base filter method saves a lot of "why is this query slower with the same params" confusion.

Watch for double-application. If a filter method also appears as a scope and you call both, you'll get a redundant WHERE (usually harmless, occasionally a wrong AND group). Keep filter methods and scopes as distinct namespaces in your head.

Extension point: centralise cross-cutting filter behaviour. If the package routes all filters through one resolver method, override it. That's your hook for logging, normalising empty values, or enforcing the allow-list in one place instead of in twelve controllers:

protected function applyFilter($query, $name, $value)
{
    Log::debug('filter.applied', ['model' => static::class, 'filter' => $name]);

    return parent::applyFilter($query, $name, $value);
}

Don't fight it for complex queries. Date-range dashboards, multi-select facets, full-text search — these want a dedicated Query Object or a package like spatie/laravel-query-builder. rmasters/filter earns its keep for straightforward, key-value refinements on CRUD index pages. If you find yourself writing filterNotStatusOrCreatedBetween, you've outgrown it; move that endpoint to explicit query construction and leave the package for the simple listings.

Pin the version. With a low-traffic package, "rmasters/filter": "^1.0" is safer than "*", and committing composer.lock is genuinely load-bearing here.

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.
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator
equipc/laravel-email-database-log
adedsec/nova-translated-labels
wazza/laravel-db-encryption
hokny/laravel-generator
vonsogt/backup
jengo/queues
jeremykenedy/laravel-observability
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums