---
title: "Building queries — Query Builder for Laravel | Roundly"
description: "Wrap a model or a prepared builder with QueryBuilder::for(), declare the allow-list, and keep chaining like any Eloquent builder."
url: https://roundly-consulting.com/open-source/docs/query-builder-for-laravel/building-queries
language: en
---

[All packages](https://roundly-consulting.com/open-source.md)

[Query Builder for Laravel](https://roundly-consulting.com/open-source/docs/query-builder-for-laravel.md)

# Building queries

Build a query for a model (or a prepared builder), declare the allow-list, then treat the result like any Eloquent builder — unknown calls forward straight through:

```php
use RoundlyConsulting\QueryBuilder\AllowedFilter;
use RoundlyConsulting\QueryBuilder\AllowedSort;
use RoundlyConsulting\QueryBuilder\QueryBuilder;

$posts = QueryBuilder::for(Post::class)
    ->allowedFilters(
        AllowedFilter::exact('status'),
        AllowedFilter::partial('title'),
        AllowedFilter::scope('published', booleans: true),
        AllowedFilter::callback('min_views', fn ($query, $value) => $query->where('views', '>=', $value)),
        AllowedFilter::trashed(),
    )
    ->allowedSorts('title', AllowedSort::field('popularity', 'views'))
    ->defaultSort('-created_at')
    ->with(['author'])
    ->paginate($request->perPage());
```

## Entry point

QueryBuilder::for(string|Builder $subject, ?Request $request = null) accepts a model class string or a prepared Eloquent builder, so you can pre-scope the query. The optional second argument overrides the request it reads from — it defaults to the current request():

```php
// A model class — starts from Post::query()
QueryBuilder::for(Post::class);

// A prepared builder — its constraints are kept
QueryBuilder::for(Post::query()->where('active', true));

// An explicit request (tests, jobs, sub-requests) instead of request()
QueryBuilder::for(Post::class, $request);
```

## In a controller

A typical list endpoint pairs the builder with a FormRequest that validates the page size (see Pagination):

```php
namespace App\Http\Controllers;

use App\Http\Requests\ListPostsRequest;
use App\Models\Post;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use RoundlyConsulting\QueryBuilder\AllowedFilter;
use RoundlyConsulting\QueryBuilder\QueryBuilder;

final class PostController
{
    public function index(ListPostsRequest $request): LengthAwarePaginator
    {
        return QueryBuilder::for(Post::class)
            ->allowedFilters('status', AllowedFilter::partial('title'))
            ->allowedSorts('title', 'created_at')
            ->defaultSort('-created_at')
            ->with(['author'])
            ->paginate($request->perPage());
    }
}
```

A request against it:

```
GET /posts?filter[status]=published&filter[title]=laravel&sort=-created_at,title&page=2&per_page=25
```

## How it applies

- Shorthand — a bare string in allowedFilters() becomes AllowedFilter::exact(); in allowedSorts() it becomes AllowedSort::field().
- Lazy, once — filters and sorts are applied the first time a call is forwarded to the builder (get, paginate, with, where, …) or you call getEloquentBuilder(), and never twice.
- Transparent forwarding — a forwarded call that returns the wrapped builder (where(), with(), …) returns the QueryBuilder itself, so chaining continues. One that returns another Eloquent builder (clone()) returns a new QueryBuilder around it with the same allow-list and applied state; anything else (getQuery() and toBase(), a relation, a collection, a paginator, a model) is returned as is.
- Separate copies — clone $builder clones the wrapped builder too, so constraints added to a copy never leak into the original.
- getEloquentBuilder() — returns the underlying Eloquent builder with filters and sorts applied, for code that needs the real Builder instance.

```php
$builder = QueryBuilder::for(Post::class)
    ->allowedFilters('status')
    ->allowedSorts('title')
    ->getEloquentBuilder();   // Illuminate\Database\Eloquent\Builder, filters + sorts applied

$count = $builder->count();
```

## Declare the allow-list first

Declare allowedFilters(), allowedSorts() and defaultSort() before any builder call. The request is applied the first time a call is forwarded to the builder, because that call may be the one that runs the query. A declaration after that point throws AllowListAlreadyApplied (a LogicException) instead of being silently ignored — whatever the request contains, so a misordered chain fails in development, not on the first client that filters:

```php
QueryBuilder::for(Post::class)
    ->allowedFilters('status')      // declare first…
    ->where('views', '>', 0)        // …then any builder call
    ->get();

QueryBuilder::for(Post::class)
    ->where('views', '>', 0)
    ->allowedFilters('status');     // throws AllowListAlreadyApplied

// Constraints that must exist before the request is applied go on the builder you pass in
QueryBuilder::for(Post::query()->where('views', '>', 0));
```

## What it leaves to you

The package covers filtering, sorting and pagination only. Include and eager-load management, sparse fieldsets, appended attributes and cursor pagination are deliberately out of scope — the package never reads such parameters from the request. Eager-load in code with with() as usual.

## Show your open-source love

This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.

[More ways to support, including crypto](https://roundly-consulting.com/support-us.md)

By donating, you agree to our [donation terms](https://roundly-consulting.com/donation-terms.md).

[Support our open source work (opens in a new tab)](https://donate.stripe.com/dRmeVe8FX5PF1Qd9pXcEw00) [Join us on Patreon (opens in a new tab)](https://www.patreon.com/cw/roundly)

## Want this built into your product?

We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.

[Get a quote in 48 hours](https://roundly-consulting.com/contact.md) [Browse all packages](https://roundly-consulting.com/open-source/docs/query-builder-for-laravel.md)
