kalimulhaq/qubuilder
A Laravel package that simplifies constructing complex database queries. It provides a fluent, reusable API for dynamic filtering, sorting, and searching of Eloquent models, reducing boilerplate while keeping query logic readable and maintainable.
qubuilder is a small, focused package, so onboarding is mostly about reading source rather than docs.
composer require kalimulhaq/qubuilder:^1.3. v1.3.2 is a patch release published with only a changelog link, so there are no documented breaking changes — bumping from 1.3.1 is a low-risk composer update. Skim the compare diff anyway if you depend on edge-case filter behaviour, since patch releases in query-building packages often fix operator/parsing bugs that change generated SQL.php artisan vendor:publish --provider="Kalimulhaq\QuBuilder\QuBuilderServiceProvider") and immediately check whether that includes a config file. If it does, commit it — most of the package's behaviour (allowed operators, default page size, request key names) tends to be config-driven.README.md → config/qubuilder.php (if present) → src/ (the entry class is usually the thing you inject or extend).$request->query('sort') / where chains and replace it with the package's builder. That is the highest-value swap and it gives you an immediate side-by-side to validate output with ->toSql() before you ship.If the README is thin, write your own 10-line usage example into the repo's docs as you discover it — you will be the documentation for the next developer.
One builder class per resource. Rather than calling the package inline from controllers, wrap it:
final class InvoiceQuery
{
public function __construct(private Request $request) {}
public function build(): Builder
{
return QuBuilder::for(Invoice::query())
->allowedFilters(['status', 'customer_id', 'created_at'])
->allowedSorts(['created_at', 'total'])
->allowedIncludes(['customer', 'lines'])
->fromRequest($this->request);
}
}
The allow-lists are the whole point — they are your injection boundary. Never pass $request->all() into a filter/sort setter.
Controller shape. Keep the controller to three lines: build, paginate, return a resource collection. Let the package own parsing and the resource own serialisation.
Compose with scopes, don't fight them. Applied scopes (tenant isolation, soft-delete policy, visibility rules) should be attached before the builder runs, so package-generated wheres land on top of them. Attaching scopes afterwards risks the builder overwriting a constraint if it uses the same column.
Whitelist at the boundary, transform inside. Validate request parameters with a FormRequest (types, enum membership, date formats) and let the builder handle structure. Doing both in the builder makes failures hard to attribute.
Testing. Assert on ->toSql() and bindings for filter/operator coverage, and assert HTTP-level behaviour (status, pagination meta, shape) for the endpoint. Query-assertion tests catch operator regressions on patch upgrades far faster than integration tests.
Reusable filter sets. If several resources share status / created_at / search, extract an allowedFilters() array into a shared trait or config so behaviour is consistent across endpoints.
ORDER BY / column positions without an allow-list are the classic failure mode for this class of package. Verify what happens with a bogus sort key before you trust it — the desired answer is "ignored" or "422", not "SQL error".includes and N+1. Eager loading via a request parameter is convenient and easy to abuse: a client can request a deep relation set on a large page. Cap allowed includes, consider ->limit() on nested relations, and check the query log with the largest page size your API permits.php artisan config:cache) will freeze that value — a very common "why did staging change?" moment. Confirm defaults explicitly in your own config rather than relying on the package's.toSql() output on two or three representative requests against a pre-upgrade capture. If the generated SQL is byte-identical, you're done.How can I help you explore Laravel packages today?