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 Bundle Laravel Package

pugx/filter-bundle

PUGXFilterBundle brings annotation-driven filtering to Symfony and Doctrine: mark filterable entity fields, then render a form that automatically builds DQL queries from user input, giving you searchable, sortable list pages without hand-writing query logic.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Start with a reality check before you run composer require. pugx/filter-bundle is a Symfony bundle: it depends on the Symfony bundle/DI-extension model, the Symfony Form component, Doctrine ORM entity metadata, and the kernel event system. There is no service provider, no config/filter.php, and no Composer auto-discovery for Laravel. In a pure Eloquent app you will get zero usable behaviour out of the install.
  • Two legitimate entry points:
    1. You already run Doctrine via laravel-doctrine/orm — you can read the source and port the pattern, but wiring the actual bundle means hand-registering a Symfony DI extension inside Laravel's container. Budget this as a real project, not a five-minute install, and expect to maintain a bridge.
    2. You want the design, not the dependency — treat the package as a reference implementation of "declarative filter set → immutable filter object → query builder" and rebuild the ~200 lines that matter in Eloquent. This is the recommended path for almost every Laravel team.
  • Read these four things first, in this order: the FilterType form base class (how a search form is declared), FilterBuilder (how a submitted form becomes a Filter), the Filter value object (the immutable result you actually apply to a query), and the annotation/attribute classes (Contains, Equals, range, boolean, date). Those four concepts are the whole package; everything else is plumbing.
  • First use case to target: an admin index screen — one search form, one list of filterable columns, one paginated result set. That's the shape the package was built for and the shape worth reproducing.
  • Version floor: pin ^1.7.1. v1.7.1 fixed two things you care about immediately — forms are now keyed by name rather than by ordinal position (so two filters on one page don't collide), and filter state is reset between requests (so long-lived workers don't leak a previous query into the next response). Neither matters if you only port the pattern, but they're the reason the pattern is trustworthy.

A minimal Eloquent port of the core idea, which is what "getting started" actually looks like for a Laravel dev:

final class FiltersQuery
{
    public function __construct(
        private array $request,
        private array $allowed, // 'name' => 'like', 'status' => 'eq', ...
    ) {}

    public function apply(Builder $query): Builder
    {
        foreach ($this->allowed as $field => $operator) {
            $value = $this->request[$field] ?? null;
            if ($value === null || $value === '') {
                continue; // blank filter must mean "unfiltered"
            }
            match ($operator) {
                'like'   => $query->where($field, 'like', "%{$value}%"),
                'prefix' => $query->where($field, 'like', "{$value}%"),
                'eq'     => $query->where($field, $value),
                'gte'    => $query->where($field, '>=', $value),
            };
        }
        return $query;
    }
}

Implementation Patterns

  • One filter class per list screen. The package's FilterType-per-resource convention exists because a shared "god filter" makes the column allow-list grow silently — someone adds a field for screen A and screen B starts accepting it too. Mirror that discipline: App\Filters\UserFilters, App\Filters\OrderFilters, each with its own explicit map of input name → operator.
  • Request → form → immutable filter → query. In the package this is handleRequest() → Filter → QueryBuilder. In Laravel it should be FormRequest (validation as the gate) → a small DTO → a QueryBuilder/Builder. Keep the DTO immutable so nothing downstream can mutate the filter mid-request; that's the property that makes the package's design hold up.
  • Hide the builder from controllers. Put the whole apply step behind a repository or scope method — User::filter($filters)->paginate() — so controllers never see where() calls and the whitelist lives in one testable place.
  • Name your filters explicitly (the v1.7.1 lesson). Previously forms were auto-named by position, so two filters of the same type on one page shared a name and submitting one could repopulate the other. In Laravel terms: derive your input names from a stable, explicit map, never from array index or field order, so generated name/id attributes stay predictable for JS and feature tests.
  • No request state in singletons. The "reset between requests" half of the fix exists because a builder could retain values across requests in a shared kernel. Anything you build around this must follow the same rule: request data lives in the request or a request-scoped DTO, never in a service property, a static, or a singleton resolved from the container. This matters on Octane in exactly the same way.
  • Decouple input names from column names. The form's field list is your only defence against someone posting ?price_gt=0 against a column you never meant to expose. Map name → Contains on a specific column; never interpolate a request key straight into where().
  • Keep sorting out of the filter. Don't overload the filter class with order-by fields. Maintain a separate, hand-written array of sortable columns, validate the sort parameter against it, and fall back to a default. Contains and ORDER BY have different performance characteristics and different trust models.
  • Custom conditions belong in a class, not a branch. The package's extension point is a new annotation/condition type; the Eloquent equivalent is a small class implementing an apply(Builder, mixed $value) contract. Reaching for if ($field === 'tags') inside a controller is the failure mode.
  • Test the two invariants that break silently: a blank filter returns the full unfiltered set (not empty), and a second request with different filter values does not inherit the first request's values. The second test is cheap and catches Octane-class bugs before production does.
  • If you are integrating the real bundle (Doctrine present): declare FooFilterType extends FilterType, one form field per condition, register the type as a service, and instantiate the FilterBuilder per-request rather than holding it in a shared service.

Gotchas and Tips

  • No Laravel auto-discovery, ever. The package ships a Symfony DependencyInjection extension and service definitions. There is no service provider to discover. If you install it into Laravel you will own the wiring, the upgrades, and the breakage — for a capability that is roughly 150 lines of your own code.
  • v1.7.1 changes generated form names. Forms are now keyed by name rather than by ordinal position. If you have JS, CSS, or feature tests targeting generated filters_... ids, re-check selectors after upgrading. Naming filters explicitly yourself removes the surprise permanently.
  • The bug this patch fixes looks like a caching problem but isn't. Before v1.7.1, a query submitted on request A could reappear on request B in any process reusing the kernel/container — Octane, Swoole, functional tests booting the kernel repeatedly. Symptom: "the search box remembers a query nobody typed." Don't go spelunking through cache config; upgrade. The same symptom class will appear in any Laravel port that stores filter state on a singleton.
  • Debugging "the filter does nothing": dump the built Filter object before it reaches the QueryBuilder, then enable SQL logging (doctrine.dbal.logging for Doctrine, DB::listen() or ->dd() for Eloquent) and diff the generated SQL against what you expected. The annotation/indirection layer makes it genuinely hard to eyeball which conditions actually fired — the value object dump is the fastest signal.
  • A blank form must equal the unfiltered set. Many conditions only add a WHERE when a value is present. Assert this explicitly; the opposite behaviour (empty result set on empty form) is a common regression when someone "simplifies" the apply loop.
  • Contains produces LIKE '%term%', which cannot use a B-tree index. On large tables prefer prefix search (term%) or a full-text index, and confirm every filterable column is actually indexed. This is the single most likely production performance surprise, and it's a code-review discipline issue rather than a package defect.
  • Doctrine metadata drift. Attribute-vs-annotation parsing and ORM 2 → 3 upgrades are a recurring breakage source for the real bundle. If you port the pattern to Eloquent you dodge this entirely; if you integrate the bundle, pin Doctrine and test after every major.
  • Project maturity is the deciding risk. Seven stars, a small maintainer footprint, community-only support, no vendor SLA, low release cadence. Treat it as a stable, narrow, presentation-layer concern — fine in a Symfony service, not something to make load-bearing, and not something to build a bespoke cross-framework bridge around. The effort of the bridge exceeds writing a Laravel-native equivalent several times over.
  • Substitute, don't adapt. If what you actually need is "declarative, form-driven filtering of queryable resources," satisfy it with a Laravel-native library (Spatie Query Builder, Eloquent Filter) or a thin in-house FiltersQuery as sketched above. Record pugx/filter-bundle as explicitly out-of-scope for Laravel work so it isn't re-proposed — and note the LGPL-3.0 licence if you do copy code rather than just the idea.
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.
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
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core