Allowlists, not request keys: what spatie/laravel-query-builder lets a caller ask Eloquent
Easily build Eloquent queries from API requests
At a glance
- What is it?
- This PHP package reads filter, include, sort, and fields parameters from an HTTP request and hands them to Eloquent only after you list them in code, which keeps request input away from the query and leaves pagination, caching, and per-user rules to you.
- Who is it for?
- Use this package when an endpoint needs a fixed set of filters, includes, sorts, and fields that you control in code and the result set is small. Skip it when you need per-user filter permissions, or a query shape the caller supplies freely, because all of that has to be written by hand on top.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly PHP, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.
Star history
- spatie/laravel-query-builder4,469 ★ · +0
| Repository | 2026-09-23 | 2026-11-14 |
|---|---|---|
| spatie/laravel-query-builder | 4.5k | 4.5k |
0 Stars in the last 52 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
One composer require, then QueryBuilder::for() and allowedFilters()
spatie/laravel-query-builder is a PHP package that sits between an incoming HTTP request and Eloquent. You add it to an existing Laravel application with composer, then point QueryBuilder at a model class. The lists you write are the only thing a caller can reach, so the first thing to understand is that this package has no opinion about your routes, only about which parameters you expose.
composer require spatie/laravel-query-builderuse Spatie\QueryBuilder\QueryBuilder;
$users = QueryBuilder::for(User::class)
->allowedFilters('name')
->get();
// all `User`s that contain the string "John" in their nameThe request that drives it is `/users?filter[name]=John`. The README shows no service provider to register and no config publish step, and it sends anything beyond the composer line to the installation notes at https://spatie.be/docs/laravel-query-builder/v7/installation-setup/. The package homepage is https://spatie.be/docs/laravel-query-builder, the default branch is main, and the license is MIT.
A filter key that is not on the list is dropped, not rejected
The name you hand to allowedFilters() is the entire contract with the request. For a route answering `/users?filter[name]=John`, the string John reaches the database because name is on the list. A request such as `/users?filter[password]=x` names a filter that is not on the list, so it cannot reach the query.
The other three lists behave the same way: allowedIncludes('posts') means include=posts loads that relation. The allowlists double as your input validation, and they only hold as long as someone remembers to update them when the schema changes.
groupOr nests filters into one OR condition instead of stacking AND
Chaining several filters gives you name AND full_name, which is rarely what a search box needs. AllowedFilter::groupOr() takes one request key and applies it to several columns under a single parenthesised condition, so `filter[q]=John` looks in both name and full_name at once.
use Spatie\QueryBuilder\AllowedFilter;
use Spatie\QueryBuilder\QueryBuilder;
$users = QueryBuilder::for(User::class)
->allowedFilters(
AllowedFilter::partial('name'),
AllowedFilter::partial('full_name'),
AllowedFilter::groupOr('q', [
AllowedFilter::partial('name'),
AllowedFilter::partial('full_name'),
]),
)
->get();
// /users?filter[q]=John
// → WHERE (name LIKE '%John%' OR full_name LIKE '%John%')
//
// /users?filter[q]=John&filter[name]=Doe
// → WHERE name LIKE '%Doe%' AND (name LIKE '%John%' OR full_name LIKE 'AllowedFilter::groupAnd() covers the other shape. Members can be any AllowedFilter type, and the shorthand value is broadcast to every member, so one request value drives all of them. The README notes that this feature follows the JSON:API fancy filters recommendation.
Includes, sorts, and fields each run on their own list
Eager loading, ordering, and column selection are three separate allowlists rather than one shared configuration, which means an endpoint grants them one at a time. Relation names go to allowedIncludes, column names go to allowedSorts, and attribute names go to allowedFields.
$users = QueryBuilder::for(User::class)
->allowedIncludes('posts')
->allowedSorts('id')
->get();With `/users?include=posts` every returned User has its posts loaded. With `/users?sort=id` the results come back in ascending id order. Field selection uses a bracketed key that names the resource, as in `/users?fields[users]=id,email`:
$users = QueryBuilder::for(User::class)
->allowedFields('id', 'email')
->get();
// the fetched `User`s will only have their id & email setQueryBuilder::for() also accepts a builder you already have
You are not forced to start from a bare model class. QueryBuilder::for() accepts an existing Eloquent builder, so request driven filtering layers on top of constraints you already set instead of replacing them.
$query = User::where('active', true);
$userQuery = QueryBuilder::for($query) // start from an existing Builder instance
->withTrashed() // use your existing scopes
->allowedIncludes('posts', 'permissions')
->where('score', '>', 42); // chain on any of Laravel's query builder methodsThe tenant, soft delete, and authorisation conditions you keep in your own scopes therefore survive whatever a caller sends, which makes this the safer entry point for multi tenant applications. Anything the package does not cover, including joins and aggregates, goes through the normal builder methods on the end of the chain. Read the code in that order: your constraints first, then the allowlists, then the query itself.
No pagination, no caching, and no per-user filter policy in the package
The README examples show no pagination step. That is fine for a lookup table behind an admin screen and a problem for a public endpoint, because `/users?filter[name]=John` will ask the database for every matching row. The limit has to come from your own code or from the database.
The same gap shows up in authorisation. allowedFilters() takes filter names, not rules about who may use them, so two endpoints sharing a list share whatever that filter exposes, including a partial match on an email address. Nothing records who asked, and no caching layer is documented, so repeated calls with identical parameters re-run the same query. Verify both gaps against the v7 documentation before you commit, and keep the shared filter lists in one place so a column that becomes sensitive is edited once.
7.3.5 shipped on 2026-09-03 and UPGRADING.md holds the jump notes
The release trail is short and current: 7.3.7 on 2026-10-26, 7.3.6 on 2026-10-19, and 7.3.5 on 2026-09-03, with the last push landing on 2026-10-26. The repository is not archived, and every documentation link in the README points at the v7 path, so v7 is the line you install.
What the README does not state is the PHP or Laravel version this package needs, and that is the first thing to check on the installation page before you pin 7.3.7 in a composer.json. UPGRADING.md carries the notes for moving between major versions and CHANGELOG.md records what changed recently. The top level of the repository also holds composer.json, config/, src/, tests/, database/, resources/, docs/, phpunit.xml.dist, phpstan.neon.dist, and phpstan-baseline.neon. To exercise the package the way its maintainers do, composer test is the only command given.
The linked docs pages, not the README, carry the feature lists
The README treats each feature family as a short example plus a link, so treat it as an index. Filtering links out to partial filters, exact filters, scope filters, custom filters, ignored values, and default filter values. Includes link out to nested relationships, relationship counts, and custom includes. Sorting links out to custom sorts and sort direction. Selecting fields has its own page.
Two details in the file itself are worth knowing. The issue tracker paragraph invites suggestions for improving the media library, which is a leftover from another Spatie package, so treat the README as a starting point rather than a specification. Security problems go to [email protected] instead of the issue tracker. The support section asks readers to mail a postcard from their hometown and links to a virtual postcard wall, and the credits name Alex Vanderbist alongside all contributors.
Editorial conclusion
Use this package when an endpoint needs a fixed set of filters, includes, sorts, and fields that you control in code and the result set is small. Skip it when you need per-user filter permissions, or a query shape the caller supplies freely, because all of that has to be written by hand on top. Before pinning 7.3.7, check the PHP and Laravel requirements on the installation page and read UPGRADING.md, since the README states neither.
Frequently asked questions
What does a query builder do in spatie/laravel-query-builder?
It turns request parameters such as `/users?filter[name]=John`, `include=posts`, `sort=id`, and `fields[users]=id,email` into Eloquent calls, and each parameter only takes effect after you pass its name to allowedFilters, allowedIncludes, allowedSorts, or allowedFields.
What is laravel query builder, and where does spatie/laravel-query-builder fit?
The package is a layer on top of Eloquent. You call QueryBuilder::for() with a model class or an existing builder, declare your allowlists, and the request parameters decide which filters, includes, sorts, and fields are applied.
What is the difference between an ORM and a query builder for spatie/laravel-query-builder?
This package does not replace Eloquent. It reads filter, include, sort, and fields parameters from the request and passes the accepted values into Eloquent, and it starts from either a model class or a builder instance you already have.
Does spatie/laravel-query-builder cover join in a laravel query builder?
No join helper appears in the README. QueryBuilder::for() gives you a builder you can chain Laravel's query builder methods on, while the documented feature set covers filtering, including relationships, sorting, and selecting fields.
laravel query builder vs raw sql: where does spatie/laravel-query-builder sit?
It writes the query through Eloquent rather than through raw statements. A caller supplied value reaches the database only after you list the corresponding filter, include, sort, or field name in code.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/spatie-laravel-query-builder)Star history chart for your README
[](https://hysenlabs.com/projects/spatie-laravel-query-builder)Was this useful?