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

Pipeline Query Collection Laravel Package

l3aro/pipeline-query-collection

A Laravel package for building Eloquent queries with pipelines: chain reusable filter classes into a clean, testable sequence, so complex query logic stays out of controllers and models and remains easy to compose, reuse, and maintain.

View on GitHub
Deep Wiki
Context7

Getting Started

  • Install with composer require l3aro/pipeline-query-collection. v1.6.0 widens the framework constraint to include Laravel 13.x, so the same install works on Laravel 10–13 (check composer.json for the exact PHP floor before bumping).
  • Look first at three things in the repo: the base query collection class (the thing you extend), the query/pipeline contract each individual query implements, and the tests/ directory — the tests are the most reliable documentation of the run order and how request input is threaded through.
  • First use case: take a fat index() action — inline when() filters, a sort switch, a search block — and split it into a collection class plus one small query class per concern. That is the entire value proposition: the controller names what queries run, each query class owns how.
// app/Queries/Users/UserCollection.php
class UserCollection extends QueryCollection
{
    protected array $queries = [
        SearchUsers::class,
        FilterByStatus::class,
        SortUsers::class,
    ];
}

// Controller
return UserResource::collection(
    UserCollection::make(User::query(), $request->validated())->paginate()
);

(Exact constructor/factory signature varies by version — confirm against the README before copying; the shape above is the pattern, not a guaranteed API.)

Implementation Patterns

One query class per axis of variation. Resist FilterUsers that handles status, role, and date range. Separate classes stay individually testable and can be recombined across collections (an admin collection and a public collection can share SearchUsers but not FilterByDeleted).

Feed it validated input, not the raw request. Pass $request->validated() (or $request->only([...])) from a FormRequest. The collection then never has to guess whether ?status= is user-supplied garbage, and your filters collapse to simple filled() checks.

class FilterByStatus implements QueryContract
{
    public function apply(Builder $query, array $input): Builder
    {
        return $query->when(
            filled($input['status'] ?? null),
            fn (Builder $q) => $q->whereIn('status', Arr::wrap($input['status']))
        );
    }
}

Apply scoping/authorization first. Put a ScopeToTenant or ScopeToUser query at the head of the list so downstream filters can only narrow an already-safe result set. Never rely on a filter class to enforce access control — a missing query parameter would silently widen it.

Sort whitelisting belongs in the sort query. Map request values to real columns through an explicit array; never interpolate orderBy($input['sort']). This is the single most common place these packages get abused.

Compose collections rather than branching inside them. A BaseUserCollection with search + sort, then AdminUserCollection extends BaseUserCollection appending FilterByDeleted and IncludeTrashed. Keeps each endpoint's behavior readable in one short array.

Test at both levels. Unit-test a single query class against a Builder and assert the compiled SQL or result set; feature-test the collection end-to-end with a handful of query-string permutations. The unit tests are where this architecture pays off — they replace most of your HTTP-level filter tests.

Use it for internal/API listing endpoints, not for one-off queries. If a listing has one filter and no growth expected, a when() is cheaper than a class. The pipeline earns its keep at ~3+ composable concerns or when the same concerns appear on several endpoints.

Gotchas and Tips

  • Order is semantics. Pipes run sequentially, so limit()/paginate() placed before orderBy gives you an arbitrary page. Put pagination at the call site, not in a query class.
  • Laravel 13 upgrade is low-risk but not zero. v1.6.0 is a constraint-only release — no API changes — so treat it as a patch-level bump. The risk is transitive: if you're jumping to Laravel 13 at the same time, do the framework upgrade first with the package pinned at ^1.5, then bump. If you are not on Laravel 13, there is no reason to rush this release.
  • Query classes should be stateless and single-use. Don't stash the request on $this in the constructor; if the collection instance is reused (or the class is resolved as a singleton in a container binding), you'll leak state between calls and get bizarre cross-request bugs that only show up under Octane.
  • Eager loading placement. A with() inside a filter class runs before later filters — usually fine, occasionally wasteful. Prefer a dedicated EagerLoadRelations query placed last, or keep with() at the call site so it's visible in the controller.
  • paginate() + distinct()/groupBy() is a known Eloquent trap, not a package one. If a filter introduces a join, verify the count query — check with ->toSql() on both the rows and count queries.
  • Debug by dumping per-pipe SQL. Bind a small listener or temporarily ray()->sql() / logger($query->toSql(), $query->getBindings()) inside the pipeline loop. Logging which query classes ran alongside the input keys makes "why did this filter not apply" a 10-second question instead of a 10-minute one.
  • Beware falsy inputs. ?status=0, ?min_price=0, and empty strings are the usual culprits — use filled()/array_key_exists rather than if ($value), or 0 filters silently no-op.
  • Extension points to reach for before forking: the base collection class (subclass it to add shared defaults), the query contract (implement it for custom behavior), and any publishable config. Only fork the package if you need to change pipeline execution itself; that's the piece most likely to conflict on upgrade.
  • Pin the major range ("^1.6") rather than *. The package is pre-2.0, so minor releases are the de-facto breaking-change channel; review each changelog before a composer update in CI.
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.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
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