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

Filterable Laravel Package

jurager/filterable

jurager/filterable adds reusable, declarative filtering to Laravel Eloquent models. Define allowed filters, apply them from request data, and keep complex query logic out of controllers for cleaner, safer APIs and admin listings.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require jurager/filterable; the service provider is auto-discovered on Laravel 9+.
  • Publish the config so you can see the parameter names and defaults before writing any code: php artisan vendor:publish --tag=filterable. Read through config/filterable.php first — it defines the query keys (sort, search key, delimiter for relations, etc.) and is the single source of truth for everything below.
  • Add the Filterable trait to the model you want to expose, and declare an explicit whitelist of filterable columns. Never rely on the implicit "all columns" behaviour in production.
use Jurager\Filterable\Traits\Filterable;

class Post extends Model
{
    use Filterable;

    protected array $filterable = ['id', 'title', 'status', 'created_at'];
    protected array $sortable   = ['id', 'created_at', 'title'];
}
  • First use case: a thin index endpoint. Put the call in the controller (or a dedicated action), not scattered through the model:
public function index(Request $request)
{
    return PostResource::collection(
        Post::query()->filter($request->query())->paginate(25)
    );
}
  • Verify with ->toSql() and ->getBindings() before you trust the output; it is the fastest way to confirm which clauses the package actually generated.

Implementation Patterns

Whitelist per resource, not per model. If two endpoints expose the same model with different permissions, define a base model with the trait and extend it (or override the filterable/sortable accessors) so each controller gets its own surface area. This keeps "admin can filter by internal_notes" out of the public endpoint.

Normalise request input before it reaches the scope. Query strings are messy — trailing commas, empty values, arrays passed as scalars. A FormRequest with array/string rules and a prepareForValidation() that drops empty values makes the filter behaviour predictable and gives you a place to log rejected keys.

Relation filters are the high-value feature; use them deliberately. Filtering on comments.author_id (via the configured relation delimiter) compiles down to whereHas. Pair it with with() for the columns you actually render, otherwise you trade a correct query for an N+1 on serialisation. For heavy relations, prefer a pre-computed column or a dedicated scope over a nested relation filter.

Range filters belong on date and numeric columns. from/to style parameters map naturally to created_at and price ranges. Index those columns (created_at almost always, plus whatever your dashboards slice on) — the package builds the WHERE, it does not add indexes for you.

Compose with scopes rather than replacing them. filter() returns an Eloquent builder, so ordering is preserved: apply tenancy scopes (->forTenant($id)) before filter(), and let the filter layer only narrow within the already-authorised set. Treat filter parameters as untrusted selection, never as authorisation.

Sorting is a separate whitelist. Keep $sortable narrower than $filterable and always append a deterministic tiebreaker (->orderBy('id')) so paginated results don't shift between pages when the sort column has duplicates.

Encapsulate recurring filter sets. For dashboards reused across endpoints, extract a small PostFilters class that takes validated input and returns a builder, so the whitelist lives next to the tests rather than inside controllers.

Test at the query level, not the response level. Assert on $query->toSql()/bindings for the tricky cases (relation filters, ranges, multiple values) — response tests will pass even when a filter is silently ignored, which is the most common failure mode.

Gotchas and Tips

  • Silently ignored parameters are the default failure mode. An unknown key is typically dropped without an error, so a typo in the frontend (?statuss=published) returns unfiltered results. Log rejected keys in development, or assert the full filter set in feature tests.
  • A too-broad $filterable is an information leak. Filtering by a column reveals its values (binary search by id, email, is_admin). Whitelist by hand and review it in code review like you would a policy.
  • Relation filters and whereHas cost. Deep relation chains generate correlated subqueries. Watch the query log on realistic data volumes and cap the relation depth you allow in the config.
  • LIKE semantics differ per driver. MySQL's default collation makes LIKE case-insensitive, PostgreSQL's does not, and wildcards in user input are not escaped for you. Normalise case in the request or use the driver-appropriate operator if you need parity across environments.
  • Config caching bites on deploy. After changing config/filterable.php, run php artisan config:clear locally and remember config:cache in production — a stale cached config makes parameter-name changes appear to do nothing.
  • Parameter name collisions. If you already have a local scopeFilter, a filter request key, or a package that owns ?sort=, resolve the conflict in config rather than monkey-patching the trait; the scope name and query keys are the intended extension points.
  • Empty strings and arrays. ?status= often means "no filter" to a user and "match empty string" to a query builder. Decide explicitly in prepareForValidation() and keep it consistent.
  • Extension points to reach for first: the published config (query keys, relation delimiter, allowed relations), overriding the trait's filterable/sortable definitions, and custom where closures for the handful of filters that need real logic. Reading the trait source is worth ten minutes — it is small, and knowing exactly which builder methods it calls stops most "why is my filter ignored" debugging.
  • Version note: 1.9.2 is a patch release with no published changelog detail beyond the diff link; pin ^1.9 and skim the diff for 1.9.0/1.9.1 if you are upgrading from 1.8.x, since trait and config changes in minors are the realistic source of surprises 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.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib