PHPackages                             teoprayoga/laravel-teorion - 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. [Search &amp; Filtering](/categories/search)
4. /
5. teoprayoga/laravel-teorion

ActiveLibrary[Search &amp; Filtering](/categories/search)

teoprayoga/laravel-teorion
==========================

Request-driven query filter package for Laravel — formalized ScopeFilterTrait pattern with whitelist, isolated scope params, and single-call ViewModel support.

v2.4.0(1mo ago)0651MITPHPPHP ^8.1CI failing

Since Jun 3Pushed 1mo agoCompare

[ Source](https://github.com/teoprayoga/laravel-teorion)[ Packagist](https://packagist.org/packages/teoprayoga/laravel-teorion)[ RSS](/packages/teoprayoga-laravel-teorion/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (10)Versions (7)Used By (0)

Laravel Teorion
===============

[](#laravel-teorion)

[![Tests](https://github.com/teoprayoga/laravel-teorion/actions/workflows/tests.yml/badge.svg)](https://github.com/teoprayoga/laravel-teorion/actions/workflows/tests.yml)[![Latest Stable Version](https://camo.githubusercontent.com/c0d71c2fbe9b69e1c47282fef482d0ebfc99ea460e25f3fc582ba0a869154ce5/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f74656f707261796f67612f6c61726176656c2d74656f72696f6e2e737667)](https://packagist.org/packages/teoprayoga/laravel-teorion)[![Total Downloads](https://camo.githubusercontent.com/aa723f3e0b6f38311813f006836507e057781cd0ef041f8588908c9c045c396a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f74656f707261796f67612f6c61726176656c2d74656f72696f6e2e737667)](https://packagist.org/packages/teoprayoga/laravel-teorion)[![License](https://camo.githubusercontent.com/20a491474515830f5d9cbda0dee50e981dfbcf11953ec776d03caedf1c8fe29a/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f6c2f74656f707261796f67612f6c61726176656c2d74656f72696f6e2e737667)](https://packagist.org/packages/teoprayoga/laravel-teorion)

Request-driven query filter package for Laravel — a formalized, secure replacement for ad-hoc query scope traits, with whitelist enforcement, isolated scope parameters, and single-call ViewModel integration.

Features
--------

[](#features)

- 🎯 **Declarative QueryFilter classes** — one per resource, central whitelist for filters/scopes/withs/sorts
- 🔒 **Whitelist enforcement** — no accidental scope/relation exposure to clients
- 🔐 **Isolated scope params** — `scopes[0][params][role_id]=3` keeps params scoped without colliding with global request
- 🔌 **Built-in filter types** — Exact, Like, MultiLike, Boolean, Null, Enum, In, Date, DateRange, Between, Range, GreaterThan, LessThan, Has, JsonContains, Callback, Scope
- 🔁 **Backward compatible** — supports legacy `scopes[]=name` format alongside the new isolated-params format
- 📑 **Dual sort format** — Spatie-style `?sort=-col,col2` AND legacy `?order_by=...&order_direction=...`
- 🗑️ **Auto soft delete handling** — `?with_trashed=1`, `?only_trashed=1` work automatically on SoftDeletes models
- 📊 **Aggregation support** — `withCount`, `withSum`, `withAvg`, `withMax`, `withMin` via `withAggregates[]`
- 📜 **Cursor pagination** — switch to cursor-based paging via `?pagination=cursor` for large datasets / infinite scroll
- 🔍 **Query audit &amp; fingerprint** — deterministic SHA-256 fingerprint + `QueryAudited` event for observability, cache key derivation, and N+1 detection
- 🛠 **Fluent filter API** — `.alias()`, `.default()`, `.required()` chainable
- 🧰 **Macro system** — register custom global filter types via `FilterMacroRegistry`
- 📖 **Scribe integration** — auto-generate API docs from `#[UsesQueryFilter]` attribute
- ✅ **Validation rule generator** — `HasQueryFilterRules` trait auto-generates FormRequest rules

Requirements
------------

[](#requirements)

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

PHP VersionLaravel 10Laravel 11Laravel 12Laravel 138.1✅———8.2✅✅✅—8.3✅✅✅✅8.4—✅✅✅Installation
------------

[](#installation)

```
composer require teoprayoga/laravel-teorion
```

(Service provider auto-discovers via Laravel package discovery.)

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

[](#quick-start)

### 1. Generate a QueryFilter class

[](#1-generate-a-queryfilter-class)

```
php artisan make:query-filter PostQueryFilter
```

Creates `app/QueryFilters/PostQueryFilter.php`.

### 2. Declare your filters

[](#2-declare-your-filters)

```
use Teoprayoga\Teorion\Filters\Filter;
use Teoprayoga\Teorion\QueryFilter;

class PostQueryFilter extends QueryFilter
{
    protected array $defaultSort = ['-created_at'];

    public function filters(): array
    {
        return [
            'search'     => Filter::multiLike(['title', 'description']),
            'status'     => Filter::enum('status', StatusEnum::class),
            'is_active'  => Filter::boolean()->default(true),
            'created_by' => Filter::exact(),
            'has_image'  => Filter::has('image'),
        ];
    }

    public function allowedScopes(): array
    {
        return ['published', 'popular'];
    }

    public function allowedWiths(): array
    {
        return ['author', 'comments', 'tags'];
    }

    public function allowedWithCounts(): array
    {
        return ['comments', 'reactions'];
    }

    public function allowedSorts(): array
    {
        return ['created_at', 'title', 'view_count'];
    }
}
```

### 3. Add the Filterable trait to your model

[](#3-add-the-filterable-trait-to-your-model)

Three ways to bind a QueryFilter (pick one):

**A. Convention (zero boilerplate)** — model `Post` auto-resolves to `App\QueryFilters\PostQueryFilter`:

```
use Teoprayoga\Teorion\Traits\Filterable;

class Post extends Model
{
    use Filterable;

    // Existing scopeXxx() methods stay here — whitelist controls which are exposed.
}
```

**B. Property override** — explicit, IDE-navigable:

```
class Post extends Model
{
    use Filterable;

    protected string $queryFilter = CustomPostQueryFilter::class;
}
```

**C. Method override** — for dynamic resolution:

```
use Teoprayoga\Teorion\QueryFilter;

class Post extends Model
{
    use Filterable;

    public function newQueryFilter(): QueryFilter
    {
        return new PostQueryFilter();
    }
}
```

Customize the convention namespace in `config/teorion.php`:

```
'query_filters_namespace' => 'App\\Filters\\Query',
```

### 4. Use in your ViewModel/Controller

[](#4-use-in-your-viewmodelcontroller)

```
public function index(GetRequest $request): mixed
{
    return Post::query()->filterAndPaginate($request);
    //                  ^^^^^^^^^^^^^^^^^^^^^^^^
    //                  applies all filters, scopes, withs, sorts,
    //                  and terminates with paginate() or get()
}

public function show(GetRequest $request, string $uuid): mixed
{
    return Post::findFiltered($request, $uuid);
}
```

Request Format
--------------

[](#request-format)

ParamExampleEffectDeclared filter`?search=lorem&status=published`Each declared filter applied if param present`scopes[]` legacy`?scopes[]=published`Calls `scopePublished($request)` with full request`scopes[N]` new`?scopes[0][name]=forStudent&scopes[0][params][role_id]=3`Calls `scopeForStudent($scopedRequest)` with isolated params`withs[]``?withs[]=author&withs[]=comments`Eager loads (whitelist enforced)`withCounts[]``?withCounts[]=comments`Count loads (whitelist enforced)`withAggregates``?withAggregates[comments][sum][]=score`Sum/avg/max/min aggregates`sort``?sort=-created_at,title`Spatie-style sort, multi-column`order_by` / `order_direction``?order_by=created_at&order_direction=desc`Legacy single-sort`is_paginate``?is_paginate=1&per_page=20`Paginate vs get`pagination``?pagination=cursor&per_page=20`Cursor pagination mode (returns `CursorPaginator`)`cursor``?cursor=eyJpZCI6MTB9`Cursor token from previous response`with_trashed``?with_trashed=1`Auto-detected on SoftDeletes models`only_trashed``?only_trashed=1`Soft-deleted only`visibles[]` / `hiddens[]``?hiddens[]=password`makeVisible / makeHidden on resultAvailable Filter Types
----------------------

[](#available-filter-types)

FilterSQL`ExactFilter``WHERE col = ?``LikeFilter``WHERE col LIKE %?%``MultiLikeFilter``WHERE (col1 LIKE %?% OR col2 LIKE %?%)``BooleanFilter``WHERE col = 1/0``NullFilter``WHERE col IS NULL` / `IS NOT NULL``EnumFilter``WHERE col = Enum::from(value)->value``InFilter``WHERE col IN (?, ?, ...)``DateFilter``WHERE DATE(col) = ?``DateRangeFilter``WHERE col BETWEEN ? AND ?``BetweenFilter``WHERE col BETWEEN ? AND ?` (from single param value array)`RangeFilter``WHERE col >= ? AND col  ?` (or `>=`)`LessThanFilter``WHERE col < ?` (or ` Filter::multiLike(['name', 'desc'])->alias('q'),
'is_active'  => Filter::boolean()->default(true),
'tenant_id'  => Filter::exact()->required(),
```

Macros (Custom Filter Types)
----------------------------

[](#macros-custom-filter-types)

```
// AppServiceProvider::boot()
FilterMacroRegistry::register('phone', function ($q, $value, $param) {
    return $q->where($param, preg_replace('/\D/', '', $value));
});

// In QueryFilter
'phone_number' => Filter::macro('phone'),
```

Scribe Integration
------------------

[](#scribe-integration)

```
// Controller
use Teoprayoga\Teorion\Attributes\UsesQueryFilter;

class PostController
{
    #[UsesQueryFilter(PostQueryFilter::class)]
    public function index(GetRequest $request): JsonResponse { ... }
}
```

Register the strategy in `config/scribe.php`:

```
'strategies' => [
    'queryParameters' => [
        Strategies\QueryParameters\GetFromInlineValidator::class,
        Strategies\QueryParameters\GetFromQueryParamTag::class,
        \Teoprayoga\Teorion\Scribe\Strategies\UsesQueryFilterStrategy::class,
    ],
],
```

Validation Rule Generator
-------------------------

[](#validation-rule-generator)

```
use Teoprayoga\Teorion\Concerns\HasQueryFilterRules;

class GetRequest extends FormRequest
{
    use HasQueryFilterRules;
    protected string $queryFilter = PostQueryFilter::class;

    public function rules(): array
    {
        return array_merge($this->queryFilterRules(), [
            // your custom rules
        ]);
    }
}
```

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

[](#configuration)

`config/teorion.php` (published via `php artisan vendor:publish --tag=teorion-config`):

```
return [
    'default_per_page'         => 10,
    'paginate_key'             => 'is_paginate',
    'per_page_key'             => 'per_page',
    'max_results_key'          => 'max_results',
    'pagination_mode_key'      => 'pagination',
    'cursor_pagination_value'  => 'cursor',
    'cursor_name'              => 'cursor',
    'query_filters_namespace'  => 'App\\QueryFilters',
    'strict_mode'              => env('APP_DEBUG', false),
    'audit' => [
        'enabled'     => env('TEORION_AUDIT_ENABLED', false),
        'log'         => env('TEORION_AUDIT_LOG', false),
        'log_channel' => env('TEORION_AUDIT_LOG_CHANNEL', null),
        'sample_rate' => (float) env('TEORION_AUDIT_SAMPLE_RATE', 1.0),  // 0.0–1.0
    ],
    'fingerprint' => [
        'algorithm'    => env('TEORION_FINGERPRINT_ALGORITHM', 'sha256'),  // sha256 | xxh3 | xxh128
        'exclude_keys' => ['_token', '_method', 'page', 'cursor', 'signature', 'expires'],
    ],
];
```

- `strict_mode=true` → throws `DisallowedScopeException` / `DisallowedWithException` / `ScopeMethodNotFoundException` on unlisted requests
- `strict_mode=false` → silently skips disallowed values (production-safe default)

Cursor Pagination
-----------------

[](#cursor-pagination)

For large datasets or infinite-scroll UIs, switch from offset to cursor pagination:

```
GET /posts?pagination=cursor&per_page=20

```

The response is a `CursorPaginator` exposing `next_cursor` / `prev_cursor` tokens. Pass via `?cursor=` to navigate.

```
// Filterable::scopeFilterAndPaginate return type
LengthAwarePaginator|CursorPaginator|Collection
```

Cursor pagination requires a deterministic `orderBy` clause — set one via `$defaultSort` in your `QueryFilter` (e.g., `protected array $defaultSort = ['-id'];`).

Query Audit &amp; Fingerprint
-----------------------------

[](#query-audit--fingerprint)

Enable structured audit for every `filterAndPaginate()` and `findFiltered()` call:

```
TEORION_AUDIT_ENABLED=true
TEORION_AUDIT_LOG=true
TEORION_AUDIT_LOG_CHANNEL=stack
```

Listen for the event:

```
use Teoprayoga\Teorion\Events\QueryAudited;

Event::listen(QueryAudited::class, function (QueryAudited $event) {
    // $event->record:
    //   fingerprint: ['hash', 'algorithm', 'payload']
    //   filter_class, model_class
    //   terminal_mode: 'paginate' | 'cursor' | 'collection' | 'find'
    //   limit, result_count, duration_ms, user_id
});
```

The **fingerprint** is a deterministic SHA-256 hash of `{filter_class, model_class, table, connection, normalized_parameters}`. Parameters are recursively `ksort`-normalized so order doesn't change the hash. Useful for:

- **Cache key derivation** — same intent → same hash → same cache entry
- **N+1 detection** — duplicate hashes within one request indicate suspicious repetition
- **Query analytics** — group slow queries by fingerprint to find optimization targets

Customize excluded parameters (defaults exclude page/cursor/CSRF tokens):

```
'fingerprint' => [
    'exclude_keys' => ['_token', '_method', 'page', 'cursor', 'signature', 'expires', 'your_custom_key'],
],
```

### Fingerprint Algorithm Choice

[](#fingerprint-algorithm-choice)

The default is `sha256` — available without extensions, deterministic across PHP versions, fast enough (&lt;1µs for typical request payloads). Hash itself is rarely the bottleneck — `json_encode` and recursive `ksort` dominate.

Three built-in algorithms (v2.4+):

AlgorithmHash lengthSpeed (relative)Notes`sha256`64 hex1× (baseline)Default. Crypto-grade, but overkill for fingerprinting`xxh3`16 hex~5–10× fasterPHP 8.1+ native via `hash()``xxh128`32 hex~3–5× fasterCollision-resistant alternative to xxh3Switch via config or env:

```
'fingerprint' => ['algorithm' => 'xxh3'],
// or: TEORION_FINGERPRINT_ALGORITHM=xxh3
```

Switching invalidates existing fingerprints — derive cache keys with the algorithm prefix to make rotation safe: `"query:{$result->algorithm}:{$result->hash}"`.

Custom algorithms (e.g., blake3 via ext-blake3) can be registered in your `AppServiceProvider`:

```
use Teoprayoga\Teorion\Fingerprint\AlgorithmInterface;
use Teoprayoga\Teorion\Fingerprint\AlgorithmRegistry;

AlgorithmRegistry::register(new class implements AlgorithmInterface {
    public function name(): string { return 'blake3'; }
    public function hash(string $payload): string { return hash('blake3', $payload); }
});
```

### Sampling

[](#sampling)

For production scale (10k+ req/min), full-rate audit can flood the event bus. Use `audit.sample_rate` (0.0–1.0):

```
TEORION_AUDIT_SAMPLE_RATE=0.01   # audit 1% of queries
```

Sampling is **deterministic per fingerprint** — same query intent always produces the same audit decision (always-on or always-off). This is intentional: random sampling would break fingerprint-based cache dedup (same query might or might not be audited). Hash-based sampling keeps decisions consistent.

### Audit Boundaries

[](#audit-boundaries)

The audit hook is wired to these teorion terminal methods:

TerminalAudited?`terminal_mode``filterAndPaginate()`✅`paginate` / `cursor` / `collection``findFiltered()`✅`find``scopeFilter()` (raw Builder)❌—`scopeFilterAudited()` (audited wrapper, v2.4+)✅`get` / `first` / `paginate` / `cursorPaginate` / `count`Direct Eloquent calls❌—For audit on raw Builder chains, swap `filter()` → `filterAudited()`:

```
Post::query()->filterAudited($request)->where('extra', $val)->get();
// → QueryAudited dispatched with terminal_mode='get'
```

### Listener Recipes

[](#listener-recipes)

The package emits events; you decide persistence and side effects. Five patterns:

**1. Persist to database** — your own audit log table:

```
class PersistQueryAudit
{
    public function handle(QueryAudited $event): void
    {
        QueryAuditLog::create([
            'fingerprint_hash' => $event->record['fingerprint']['hash'],
            'filter_class'     => $event->record['filter_class'],
            'duration_ms'      => $event->record['duration_ms'],
            'user_id'          => $event->record['user_id'],
            'recorded_at'      => now(),
        ]);
    }
}
```

**2. Slack alert on slow queries (&gt;500ms):**

```
Event::listen(QueryAudited::class, function (QueryAudited $event) {
    if ($event->record['duration_ms'] > 500) {
        Notification::route('slack', config('alerts.slack_webhook'))
            ->notify(new SlowQueryAlert($event->record));
    }
});
```

**3. Redis dedup cache (memoize identical queries):**

```
Event::listen(QueryAudited::class, function (QueryAudited $event) {
    $key = 'query:' . $event->record['fingerprint']['hash'];
    Redis::setex($key, 60, json_encode($event->record));
});
```

**4. N+1 detection within request:**

```
Event::listen(QueryAudited::class, function (QueryAudited $event) {
    static $seen = [];
    $hash = $event->record['fingerprint']['hash'];
    $seen[$hash] = ($seen[$hash] ?? 0) + 1;
    if ($seen[$hash] > 3) {
        Log::warning('Suspected N+1 — same query repeated', [
            'hash'  => $hash,
            'count' => $seen[$hash],
            'class' => $event->record['filter_class'],
        ]);
    }
});
```

**5. Sentry breadcrumb (low-overhead observability):**

```
Event::listen(QueryAudited::class, function (QueryAudited $event) {
    \Sentry\addBreadcrumb(new \Sentry\Breadcrumb(
        level: 'info',
        type: 'query',
        category: 'teorion',
        message: "Query: {$event->record['filter_class']}",
        metadata: ['duration_ms' => $event->record['duration_ms']],
    ));
});
```

Testing
-------

[](#testing)

```
composer test
```

Support
-------

[](#support)

If this package saves you time, consider supporting its development 🙏

[![Sponsor on GitHub](https://camo.githubusercontent.com/a51267a55d529338abcb445f92a41401ee9e4395a6c6b9019ab37ceb12149781/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f53706f6e736f722d4769744875622d4541344141413f6c6f676f3d67697468756273706f6e736f7273266c6f676f436f6c6f723d7768697465)](https://github.com/sponsors/teoprayoga)[![Ko-fi](https://camo.githubusercontent.com/339aff6082a7504833522141edaa717cb414682bddf2331cd8ec2f3dfc297702/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4b6f2d2d66692d4275792532306d6525323061253230636f666665652d4646354535423f6c6f676f3d6b6f6669266c6f676f436f6c6f723d7768697465)](https://ko-fi.com/teoprayoga)[![Saweria](https://camo.githubusercontent.com/5c6c56e0d7f301adf58821c163b187de18e8de932ba74bf82c9d92725e278446/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f536177657269612d44756b756e672d4641414230303f6c6f676f436f6c6f723d7768697465)](https://saweria.co/teoprayoga)

Maintained by **Teo Prayoga** ([@teoprayoga](https://github.com/teoprayoga)).

License
-------

[](#license)

MIT

###  Health Score

43

—

FairBetter than 89% of packages

Maintenance91

Actively maintained with recent releases

Popularity19

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 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 ~0 days

Total

6

Last Release

52d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/36a7d4a48df13c4d526004c150f3728c4c4126f132f6740b8603a3255ea80ba8?d=identicon)[teoprayoga](/maintainers/teoprayoga)

---

Top Contributors

[![teoprayoga](https://avatars.githubusercontent.com/u/19910443?v=4)](https://github.com/teoprayoga "teoprayoga (7 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/teoprayoga-laravel-teorion/health.svg)

```
[![Health](https://phpackages.com/badges/teoprayoga-laravel-teorion/health.svg)](https://phpackages.com/packages/teoprayoga-laravel-teorion)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3345.3M347](/packages/psalm-plugin-laravel)[mike-bronner/laravel-model-caching

Automatic caching for Eloquent models.

2.4k96.5k1](/packages/mike-bronner-laravel-model-caching)[api-platform/laravel

API Platform support for Laravel

58174.6k17](/packages/api-platform-laravel)[pressbooks/pressbooks

Pressbooks is an open source book publishing tool built on a WordPress multisite platform. Pressbooks outputs books in multiple formats, including PDF, EPUB, web, and a variety of XML flavours, using a theming/templating system, driven by CSS.

45444.2k1](/packages/pressbooks-pressbooks)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)

PHPackages © 2026

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