PHPackages                             jackardios/laravel-query-wizard - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [API Development](/categories/api)
4. /
5. jackardios/laravel-query-wizard

ActiveLibrary[API Development](/categories/api)

jackardios/laravel-query-wizard
===============================

Laravel Query Wizard

v2.1.3(1y ago)07901MITPHPPHP ^8.1CI passing

Since Jun 28Pushed 2mo ago1 watchersCompare

[ Source](https://github.com/Jackardios/laravel-query-wizard)[ Packagist](https://packagist.org/packages/jackardios/laravel-query-wizard)[ Docs](https://github.com/jackardios/laravel-query-wizard)[ RSS](/packages/jackardios-laravel-query-wizard/feed)WikiDiscussions master Synced 1mo ago

READMEChangelog (9)Dependencies (5)Versions (13)Used By (1)

Laravel Query Wizard
====================

[](#laravel-query-wizard)

Build Eloquent queries from API request parameters. Filter, sort, include relationships, select fields, and append computed attributes — all from query string parameters.

[![Latest Version on Packagist](https://camo.githubusercontent.com/3a64a3a469bb30212ca1382677da87d8b9104290a3769fbd367daacd792d5653/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f6a61636b617264696f732f6c61726176656c2d71756572792d77697a6172642e737667)](https://packagist.org/packages/jackardios/laravel-query-wizard)[![License](https://camo.githubusercontent.com/9f3ce65edb0ef11c6acf42cbf582684760f3e3d93465017d5f30f5c6acf9ef5e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f6a61636b617264696f732f6c61726176656c2d71756572792d77697a6172642e737667)](https://packagist.org/packages/jackardios/laravel-query-wizard)[![CI](https://github.com/jackardios/laravel-query-wizard/actions/workflows/ci.yml/badge.svg)](https://github.com/jackardios/laravel-query-wizard/actions)

Why Use Query Wizard?
---------------------

[](#why-use-query-wizard)

Building APIs often requires handling complex query parameters for filtering, sorting, and including relationships. Without a proper solution, you end up with:

- Repetitive boilerplate code in every controller
- Inconsistent parameter handling across endpoints
- Security vulnerabilities from unvalidated user input
- Tight coupling between request handling and business logic

**Query Wizard solves these problems** by providing a clean, declarative API that:

- Automatically parses request parameters
- Validates and whitelists allowed operations
- Applies filters, sorts, includes, fields, and appends to your queries
- Protects against resource exhaustion attacks with built-in limits
- Supports custom filter/sort/include implementations

Installation
------------

[](#installation)

```
composer require jackardios/laravel-query-wizard
```

The package uses Laravel's auto-discovery, so no additional setup is required.

### Publish Configuration (Optional)

[](#publish-configuration-optional)

```
php artisan vendor:publish --provider="Jackardios\QueryWizard\QueryWizardServiceProvider" --tag="config"
```

Quick Start
-----------

[](#quick-start)

```
use App\Models\User;
use Jackardios\QueryWizard\Eloquent\EloquentQueryWizard;

public function index()
{
    $users = EloquentQueryWizard::for(User::class)
        ->allowedFilters('name', 'email', 'status')
        ->allowedSorts('name', 'created_at')
        ->allowedIncludes('posts', 'profile')
        ->get();

    return response()->json($users);
}
```

Now your API supports requests like:

```
GET /users?filter[name]=John&filter[status]=active&sort=-created_at&include=posts

```

Table of Contents
-----------------

[](#table-of-contents)

- [Basic Usage](#basic-usage)
- [Filtering](#filtering)
- [Sorting](#sorting)
- [Including Relationships](#including-relationships)
- [Selecting Fields](#selecting-fields)
- [Appending Attributes](#appending-attributes)
- [Resource Schemas](#resource-schemas)
- [ModelQueryWizard](#modelquerywizard)
- [Security](#security)
- [Configuration](#configuration)
- [Error Handling](#error-handling)
- [Advanced Usage](#advanced-usage)
- [API Reference](#api-reference)
- [Comparison with spatie/laravel-query-builder](#comparison-with-spatielaravel-query-builder)

Basic Usage
-----------

[](#basic-usage)

### Creating a Query Wizard

[](#creating-a-query-wizard)

```
use Jackardios\QueryWizard\Eloquent\EloquentQueryWizard;

// From a model class
$wizard = EloquentQueryWizard::for(User::class);

// From an existing query builder
$wizard = EloquentQueryWizard::for(User::where('active', true));

// From a relation
$wizard = EloquentQueryWizard::for($user->posts());
```

### Executing Queries

[](#executing-queries)

```
// Get all results
$users = $wizard->get();

// Get first result
$user = $wizard->first();
$user = $wizard->firstOrFail();

// Paginate results
$users = $wizard->paginate(15);
$users = $wizard->simplePaginate(15);
$users = $wizard->cursorPaginate(15);

// Get the underlying query builder
$query = $wizard->toQuery();
```

### Configuration Order

[](#configuration-order)

Configuration methods (`allowedFilters`, `allowedSorts`, etc.) **must be called before** query builder methods (`where`, `orderBy`, etc.):

```
// ✅ Correct: configuration → builder methods → execution
EloquentQueryWizard::for(User::class)
    ->allowedFilters('name')        // configuration
    ->allowedSorts('created_at')    // configuration
    ->where('active', true)         // builder method
    ->get();                        // execution

// ❌ Wrong: throws LogicException
EloquentQueryWizard::for(User::class)
    ->where('active', true)
    ->allowedFilters('name');       // LogicException!
```

For base query scopes, pass a pre-configured query to `for()`:

```
EloquentQueryWizard::for(User::where('active', true))
    ->allowedFilters('name')
    ->get();
```

Filtering
---------

[](#filtering)

Filters allow API consumers to narrow down results based on specific criteria.

### Basic Filters

[](#basic-filters)

```
use Jackardios\QueryWizard\Eloquent\EloquentFilter;

EloquentQueryWizard::for(User::class)
    ->allowedFilters(
        'name',                              // Exact match (string shorthand)
        'email',                             // Exact match (string shorthand)
        EloquentFilter::exact('status'),     // Explicit exact filter
        EloquentFilter::partial('bio'),      // LIKE %value%
    )
    ->get();
```

**Request:** `GET /users?filter[name]=John&filter[bio]=developer`

### Available Filter Types

[](#available-filter-types)

TypeFactoryRequest ExampleExact`EloquentFilter::exact('status')``?filter[status]=active`Partial`EloquentFilter::partial('name')``?filter[name]=john` (LIKE %john%)Scope`EloquentFilter::scope('popular')``?filter[popular]=5000`Trashed`EloquentFilter::trashed()``?filter[trashed]=with|only`Null`EloquentFilter::null('deleted_at')``?filter[deleted_at]=true` (IS NULL)Range`EloquentFilter::range('price')``?filter[price][min]=10&filter[price][max]=100`Date Range`EloquentFilter::dateRange('created_at')``?filter[created_at][from]=2024-01-01&filter[created_at][to]=2024-12-31`JSON Contains`EloquentFilter::jsonContains('tags')``?filter[tags]=laravel,php`Operator`EloquentFilter::operator('age', FilterOperator::GREATER_THAN)``?filter[age]=18` (age &gt; 18)Operator (dynamic)`EloquentFilter::operator('price', FilterOperator::DYNAMIC)``?filter[price]=>=100` (price &gt;= 100)Callback`EloquentFilter::callback('custom', fn($q, $v, $p) => ...)``?filter[custom]=value`Passthrough`EloquentFilter::passthrough('context')`Captured but not applied### Filter Options

[](#filter-options)

All filters support fluent modifiers:

```
EloquentFilter::exact('status')
    ->alias('state')                           // URL parameter name: ?filter[state]=...
    ->default('active')                        // Default value when not in request
    ->prepareValueWith(fn($v) => strtolower($v))  // Transform before applying
    ->when(fn($v) => $v !== 'all')             // Skip filter if returns false
    ->asBoolean()                              // Convert 'true'/'1'/'yes' to bool
```

**Filter-specific modifiers:**

```
// Range filter
EloquentFilter::range('price')->minKey('from')->maxKey('to')

// Date range filter
EloquentFilter::dateRange('created_at')
    ->fromKey('start')->toKey('end')
    ->dateFormat('Y-m-d')

// JSON contains filter
EloquentFilter::jsonContains('tags')->matchAny()  // Default: matchAll()

// Null filter
EloquentFilter::null('deleted_at')->withInvertedLogic()  // IS NOT NULL

// Scope filter
EloquentFilter::scope('byAuthor')->withModelBinding()  // Load model by ID
```

### Relation Filtering

[](#relation-filtering)

Filters with dot notation automatically use `whereHas`:

```
EloquentFilter::exact('posts.status')  // Filters users by their posts' status

// Disable this behavior:
EloquentFilter::exact('posts.status')->withoutRelationConstraint()
```

Sorting
-------

[](#sorting)

Allow API consumers to sort results.

### Basic Sorts

[](#basic-sorts)

```
use Jackardios\QueryWizard\Eloquent\EloquentSort;

EloquentQueryWizard::for(User::class)
    ->allowedSorts('name', 'created_at', EloquentSort::field('email'))
    ->defaultSorts('-created_at')  // Applied when no sort in request
    ->get();
```

**Request:** `?sort=name` (asc), `?sort=-name` (desc), `?sort=-created_at,name` (multiple)

### Available Sort Types

[](#available-sort-types)

TypeFactoryDescriptionField`EloquentSort::field('created_at')`Sort by columnCount`EloquentSort::count('posts')`Sort by relationship countRelation`EloquentSort::relation('orders', 'total', 'sum')`Sort by aggregate (min, max, sum, avg, count, exists)Callback`EloquentSort::callback('custom', fn($q, $dir, $p) => ...)`Custom logicIncluding Relationships
-----------------------

[](#including-relationships)

Eager load relationships based on request parameters.

### Basic Includes

[](#basic-includes)

```
use Jackardios\QueryWizard\Eloquent\EloquentInclude;

EloquentQueryWizard::for(User::class)
    ->allowedIncludes(
        'posts',                               // Relationship (string shorthand)
        'postsCount',                          // Count (auto-detected by suffix)
        EloquentInclude::exists('subscription'),
    )
    ->defaultIncludes('profile')               // Used when ?include is not provided
    ->get();
```

**Request:** `?include=posts,postsCount,subscriptionExists`

### Available Include Types

[](#available-include-types)

TypeFactoryDescriptionRelationship`EloquentInclude::relationship('posts')`Eager load with `with()`Count`EloquentInclude::count('posts')`Load count with `withCount()`Exists`EloquentInclude::exists('posts')`Check existence with `withExists()`Callback`EloquentInclude::callback('custom', fn($q, $rel) => ...)`Custom logicIncludes ending with "Count" or "Exists" are auto-detected as count/exists includes.

Selecting Fields
----------------

[](#selecting-fields)

Allow sparse fieldsets (JSON:API compatible).

```
EloquentQueryWizard::for(User::class)
    ->allowedFields('id', 'name', 'email', 'posts.id', 'posts.title')
    ->get();
```

**Request:** `?fields[user]=id,name&fields[posts]=id,title` or `?fields=id,name`

### Relation Fields

[](#relation-fields)

Use **relation name** as the key, not table name:

```
// Model: Task with createdBy(): BelongsTo
EloquentQueryWizard::for(Task::class)
    ->allowedIncludes('createdBy')
    ->allowedFields('id', 'title', 'createdBy.id', 'createdBy.name')
    ->get();

// ✅ ?fields[createdBy]=id,name
// ❌ ?fields[users]=id,name — won't work
```

### Relation Field Modes

[](#relation-field-modes)

```
// config/query-wizard.php
'optimizations' => [
    'relation_select_mode' => 'safe',  // 'safe' (recommended) or 'off'
],
```

**Safe mode** (default): Automatically injects foreign keys for eager loading and protects accessors.

**Off mode**: No automatic handling — you must include all required FK columns manually.

Appending Attributes
--------------------

[](#appending-attributes)

Append computed model attributes (accessors) to results.

```
// Model
class User extends Model
{
    protected function fullName(): Attribute
    {
        return Attribute::get(fn() => "{$this->first_name} {$this->last_name}");
    }
}

// Query Wizard
EloquentQueryWizard::for(User::class)
    ->allowedAppends('full_name', 'posts.reading_time')
    ->defaultAppends('full_name')
    ->get();
```

**Request:** `?append=full_name,posts.reading_time`

Resource Schemas
----------------

[](#resource-schemas)

For larger applications, use Resource Schemas to define all query capabilities in one place.

### Creating a Schema

[](#creating-a-schema)

```
use Jackardios\QueryWizard\Schema\ResourceSchema;
use Jackardios\QueryWizard\Contracts\QueryWizardInterface;

class UserSchema extends ResourceSchema
{
    public function model(): string
    {
        return User::class;
    }

    public function filters(QueryWizardInterface $wizard): array
    {
        return ['name', EloquentFilter::exact('status')];
    }

    public function sorts(QueryWizardInterface $wizard): array
    {
        return ['name', 'created_at'];
    }

    public function includes(QueryWizardInterface $wizard): array
    {
        return ['posts', 'profile', 'postsCount'];
    }

    public function fields(QueryWizardInterface $wizard): array
    {
        return ['id', 'name', 'email', 'status'];
    }

    public function appends(QueryWizardInterface $wizard): array
    {
        return ['full_name'];
    }

    public function defaultSorts(QueryWizardInterface $wizard): array
    {
        return ['-created_at'];
    }

    public function defaultFilters(QueryWizardInterface $wizard): array
    {
        return ['status' => 'active'];  // Applied when filter is absent
    }
}
```

### Using Schemas

[](#using-schemas)

```
// With EloquentQueryWizard
$users = EloquentQueryWizard::forSchema(UserSchema::class)->get();

// With ModelQueryWizard (same schema!)
$user = User::find(1);
$processed = ModelQueryWizard::for($user)->schema(UserSchema::class)->process();
```

### Schema Overrides

[](#schema-overrides)

```
EloquentQueryWizard::forSchema(UserSchema::class)
    ->disallowedFilters('status')        // Remove from schema
    ->disallowedIncludes('posts')
    ->allowedAppends('extra')            // Add to schema
    ->get();
```

### Wildcard Support in disallowed\*()

[](#wildcard-support-in-disallowed)

PatternMeaning`'*'`Block everything`'posts.*'`Block direct children only`'posts'`Block relation and all descendants### Context-Aware Schemas

[](#context-aware-schemas)

Schema methods receive the wizard instance for conditional logic:

```
public function includes(QueryWizardInterface $wizard): array
{
    $includes = ['posts', 'profile'];

    // Count/exists only work with EloquentQueryWizard
    if ($wizard instanceof EloquentQueryWizard) {
        $includes[] = EloquentInclude::count('posts');
    }

    return $includes;
}
```

ModelQueryWizard
----------------

[](#modelquerywizard)

For processing already-loaded model instances. Handles includes, fields, and appends — **not** filters or sorts.

```
use Jackardios\QueryWizard\ModelQueryWizard;

$user = User::find(1);

$processed = ModelQueryWizard::for($user)
    ->allowedIncludes('posts', 'comments')
    ->allowedFields('id', 'name', 'email')
    ->allowedAppends('full_name')
    ->process();
```

FeatureBehaviorIncludesLoads missing with `loadMissing()`FieldsHides non-requested with `makeHidden()`AppendsAdds with `append()`Filters/SortsIgnoredSecurity
--------

[](#security)

### Request Limits

[](#request-limits)

Built-in protection against resource exhaustion attacks:

SettingDefaultDescription`max_include_depth`3Max nesting (e.g., `posts.comments.author` = 3)`max_includes_count`10Max includes per request`max_filters_count`20Max filters per request`max_appends_count`10Max appends per request`max_sorts_count`5Max sorts per requestConfigure in `config/query-wizard.php`. Set to `null` to disable.

### ScopeFilter Model Binding

[](#scopefilter-model-binding)

By default, `ScopeFilter` passes values as-is. Enable model binding with caution:

```
EloquentFilter::scope('byAuthor')->withModelBinding()
```

**Warning:** Model binding resolves by ID **without authorization checks**. Add checks in your scope if needed.

Configuration
-------------

[](#configuration)

Key configuration options (`config/query-wizard.php`):

```
return [
    'parameters' => [
        'includes' => 'include',   // ?include=posts
        'filters' => 'filter',     // ?filter[name]=John
        'sorts' => 'sort',         // ?sort=-created_at
        'fields' => 'fields',      // ?fields[user]=id,name
        'appends' => 'append',     // ?append=full_name
    ],

    'count_suffix' => 'Count',     // postsCount → count include
    'exists_suffix' => 'Exists',   // postsExists → exists include

    'disable_invalid_filter_query_exception' => false,  // Throw on invalid filter
    // ... similar for sort, include, field, append

    'request_data_source' => 'query_string',  // 'query_string' or 'body'
    'apply_filter_default_on_null' => false,  // Apply default() when filter value is null/empty

    'naming' => [
        'convert_parameters_to_snake_case' => false,  // ?filter[firstName] → first_name
    ],

    'optimizations' => [
        'relation_select_mode' => 'safe',  // 'safe' or 'off'
    ],

    'limits' => [
        'max_include_depth' => 3,
        'max_includes_count' => 10,
        'max_filters_count' => 20,
        'max_appends_count' => 10,
        'max_sorts_count' => 5,
        'max_append_depth' => 3,
    ],
];
```

Error Handling
--------------

[](#error-handling)

All exceptions extend `InvalidQuery` (extends Symfony's `HttpException`):

ExceptionDescription`InvalidFilterQuery`Unknown filter`InvalidSortQuery`Unknown sort`InvalidIncludeQuery`Unknown include`InvalidFieldQuery`Unknown field`InvalidAppendQuery`Unknown append`MaxFiltersCountExceeded`Too many filters`MaxIncludeDepthExceeded`Include nesting too deep...(similar for other limits)### Global Handler (Laravel 11+)

[](#global-handler-laravel-11)

```
// bootstrap/app.php
->withExceptions(function (Exceptions $exceptions) {
    $exceptions->render(function (InvalidQuery $e) {
        return response()->json([
            'error' => class_basename($e),
            'message' => $e->getMessage(),
        ], $e->getStatusCode());
    });
})
```

Advanced Usage
--------------

[](#advanced-usage)

### Batch Processing

[](#batch-processing)

All execution methods apply post-processing (field masking, appends) automatically:

```
$wizard->get();
$wizard->paginate(15);
$wizard->chunk(100, fn($users) => ...);
$wizard->lazy()->each(fn($user) => ...);
```

### Manual Post-Processing

[](#manual-post-processing)

For methods not wrapped by wizard (`find()`, `findMany()`):

```
$user = $wizard->toQuery()->find($id);
$wizard->applyPostProcessingTo($user);
```

### Laravel Octane

[](#laravel-octane)

Fully compatible. `QueryParametersManager` uses `scoped()` binding for per-request instances.

API Reference
-------------

[](#api-reference)

See [docs/api-reference.md](docs/api-reference.md) for complete method reference.

Comparison with spatie/laravel-query-builder
--------------------------------------------

[](#comparison-with-spatielaravel-query-builder)

FeatureQuery WizardSpatie**Filters**Exact, Partial, Scope, Trashed, CallbackYesYesRange, Date Range, Null, JSON ContainsYesNoPassthrough, Conditional (`when()`)YesNoValue transformation (`prepareValueWith()`)YesNo**Sorts**Field, CallbackYesYesRelationship count/aggregateYesNo**Includes**Relationship, Count, Exists, CallbackYesYesDefault includesYesNo**Appends**Appends with nestingYesNo**Architecture**Resource SchemasYesNo`disallowed*()` methodsYesNoModelQueryWizardYesNo**Security**Request limitsYesNoRequirements
------------

[](#requirements)

- PHP 8.1+
- Laravel 10, 11, or 12

Testing
-------

[](#testing)

```
composer test
```

Upgrading
---------

[](#upgrading)

See [UPGRADE.md](UPGRADE.md) for migration guides between versions.

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE) for more information.

Credits
-------

[](#credits)

- [Salavat Salakhutdinov](https://github.com/jackardios)
- Inspired by [spatie/laravel-query-builder](https://github.com/spatie/laravel-query-builder) by [Spatie](https://spatie.be)

###  Health Score

42

—

FairBetter than 90% of packages

Maintenance69

Regular maintenance activity

Popularity15

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity63

Established project with proven stability

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~126 days

Recently: every ~68 days

Total

11

Last Release

151d ago

Major Versions

v0.1.2 → v1.0.02024-01-23

v1.0.1 → v2.0.22025-02-17

PHP version history (2 changes)v0.1.0PHP ^8.0

v1.0.0PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/d502bd9c7502e3464a661326548cb3698cf0a5dfc25b45fc4b93ee22fd1a23d2?d=identicon)[jackardios](/maintainers/jackardios)

---

Top Contributors

[![Jackardios](https://avatars.githubusercontent.com/u/24757335?v=4)](https://github.com/Jackardios "Jackardios (181 commits)")

---

Tags

json-apilaravelphpquery-builderjsonapilaravellaravel-query-builderquerybuilderquery builderJSON-APIwizardjackardioslaravel-query-wizard

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/jackardios-laravel-query-wizard/health.svg)

```
[![Health](https://phpackages.com/badges/jackardios-laravel-query-wizard/health.svg)](https://phpackages.com/packages/jackardios-laravel-query-wizard)
```

###  Alternatives

[cloudcreativity/laravel-json-api

JSON API (jsonapi.org) support for Laravel applications.

7881.1M5](/packages/cloudcreativity-laravel-json-api)[aimeos/aimeos-laravel

Cloud native, API first Laravel eCommerce package with integrated AI for ultra-fast online shops, marketplaces and complex B2B projects

8.6k214.7k3](/packages/aimeos-aimeos-laravel)[joskolenberg/laravel-jory

Create a flexible API for your Laravel application using json based queries.

4513.5k](/packages/joskolenberg-laravel-jory)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
