PHPackages                             omoba/laravel-queryable - 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. [Database &amp; ORM](/categories/database)
4. /
5. omoba/laravel-queryable

ActiveLibrary[Database &amp; ORM](/categories/database)

omoba/laravel-queryable
=======================

Declarative search, sort, and filter scopes for Eloquent models — with relationship traversal.

v0.1.0(1w ago)01↑2900%MITPHPPHP ^8.2CI passing

Since Jun 2Pushed 1w agoCompare

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

READMEChangelogDependencies (6)Versions (2)Used By (0)

laravel-queryable
=================

[](#laravel-queryable)

Declarative `search`, `filter`, and `sort` scopes for Eloquent models — with first-class support for relationship traversal via dot notation.

```
User::search('john')
    ->filter([
        'status'           => 'active,pending',
        'created_at'       => ['from' => '2025-01-01', 'to' => '2025-12-31'],
        'company.industry' => 'fintech',
    ])
    ->sort('-created_at,name')
    ->paginate();
```

→ [Full documentation](docs/usage.md)

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

[](#requirements)

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

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

[](#installation)

```
composer require omoba/laravel-queryable
```

The service provider is auto-discovered. Optionally publish the config:

```
php artisan vendor:publish --tag=queryable-config
```

---

Quick start
-----------

[](#quick-start)

Add the `Queryable` trait to your model and implement the three declaration methods:

```
use Omoba\LaravelQueryable\Concerns\Queryable;
use Omoba\LaravelQueryable\Operators\FilterOperator;

class User extends Model
{
    use Queryable;

    public function searchable(): array
    {
        return ['name', 'email', 'company.name'];
    }

    public function filterable(): array
    {
        return [
            'status'     => FilterOperator::In,
            'created_at' => FilterOperator::DateRange,
        ];
    }

    public function sortable(): array
    {
        return ['name', 'created_at'];
    }
}
```

Then in your controller:

```
public function index(Request $request): JsonResponse
{
    return response()->json(
        User::query()
            ->search($request->string('q')->toString() ?: null)
            ->filter($request->array('filter'))
            ->sort($request->string('sort')->toString() ?: null)
            ->paginate($request->integer('per_page', 25))
    );
}
```

---

Traits
------

[](#traits)

Use `Queryable` for all three features, or mix in individual traits:

TraitScopes added`Queryable``search`, `searchEncrypted`, `filter`, `filterHaving`, `sort``Searchable``search`, `searchEncrypted``Filterable``filter`, `filterHaving``Sortable``sort````
use Omoba\LaravelQueryable\Concerns\Filterable;
use Omoba\LaravelQueryable\Concerns\Sortable;

class Product extends Model
{
    use Filterable, Sortable;

    // No searchable() needed — Searchable trait is not used
}
```

Each trait requires you to implement its declaration method(s). The compiler will tell you which ones are missing.

---

Declaration methods
-------------------

[](#declaration-methods)

### `searchable(): array`

[](#searchable-array)

Required by `Searchable`. Return column names (or dot-notation relation paths) to include in substring search.

```
public function searchable(): array
{
    return [
        'name',
        'email',
        'company.name',        // one level deep
        'team.company.name',   // two levels deep
    ];
}
```

### `searchableEncrypted(): array`

[](#searchableencrypted-array)

Optional — defaults to `[]`. Columns that store SHA-256 hashed values. Used by `searchEncrypted()`.

```
public function searchableEncrypted(): array
{
    return ['phone_hash'];
}
```

### `hashSearchTerm(string $term): string`

[](#hashsearchtermstring-term-string)

Optional override. Defaults to `hash('sha256', $term)`. Override to change the hashing strategy.

```
public function hashSearchTerm(string $term): string
{
    return hash('sha256', strtolower(trim($term)));
}
```

### `filterable(): array`

[](#filterable-array)

Required by `Filterable`. Maps field names to filter operators. Three declaration styles:

```
public function filterable(): array
{
    return [
        // Explicit operator via enum
        'name'             => FilterOperator::Like,
        'email'            => FilterOperator::Exact,
        'status'           => FilterOperator::In,
        'created_at'       => FilterOperator::DateRange,
        'archived_at'      => FilterOperator::Null,
        'amount'           => FilterOperator::Gte,

        // Explicit operator via string (case-insensitive)
        'score'            => 'gte',

        // No operator — inferred from the incoming value shape at runtime
        'category',
        'updated_at',
    ];
}
```

When no operator is declared, the package infers one: an array with `from`/`to` keys becomes `between`; anything else becomes `exact`.

Dot notation works the same as in `searchable()`:

```
public function filterable(): array
{
    return [
        'company.industry' => FilterOperator::Exact,
        'team.company.name' => FilterOperator::Like,
    ];
}
```

### `having(): array`

[](#having-array)

Optional — defaults to `[]`. Same shape as `filterable()`, but for columns in a `HAVING` clause (e.g. aggregates from `withCount` / `selectRaw`). Used by `filterHaving()`.

```
public function having(): array
{
    return [
        'posts_count' => FilterOperator::Between,
    ];
}
```

### `sortable(): array`

[](#sortable-array)

Required by `Sortable`. An indexed list of column names permitted for sorting.

```
public function sortable(): array
{
    return ['name', 'created_at', 'email'];
}
```

---

Scopes
------

[](#scopes)

### `search(?string $term)`

[](#searchstring-term)

Case-insensitive substring match across all columns declared in `searchable()`. Columns in related tables use `orWhereHas`. Returns the unmodified query if `$term` is `null` or empty.

Uses `ILIKE` on PostgreSQL and `LIKE` on all other drivers.

```
User::search('john')->get();
// WHERE (name LIKE '%john%' OR email LIKE '%john%' OR EXISTS (SELECT ... company.name LIKE '%john%'))
```

### `searchEncrypted(?string $term)`

[](#searchencryptedstring-term)

Hashes `$term` (default: SHA-256) and performs an exact match against columns in `searchableEncrypted()`. Returns the unmodified query if `$term` is null or empty.

```
User::searchEncrypted('+15550100')->first();
// WHERE phone_hash = 'a1b2c3...'
```

### `filter(?array $filters)`

[](#filterarray-filters)

Applies `WHERE` clauses for each key in `$filters` against the `filterable()` map. Silently skips `null` and empty-string values.

```
User::filter([
    'status'     => 'active',
    'created_at' => ['from' => '2025-01-01', 'to' => '2025-12-31'],
])->get();
```

### `filterHaving(?array $filters)`

[](#filterhavingarray-filters)

Same as `filter()` but emits `HAVING` clauses. Use after aggregating with `withCount`, `selectRaw`, etc.

```
User::withCount('posts')
    ->filterHaving(['posts_count' => ['from' => 5]])
    ->get();
// HAVING posts_count >= 5
```

### `sort(string|array|null $spec)`

[](#sortstringarraynull-spec)

Applies `ORDER BY` for each field in `$spec`. The field must be declared in `sortable()`. Returns the unmodified query on `null` or empty input.

```
// String form — prefix with '-' for descending
User::sort('-created_at,name')->get();
// ORDER BY created_at DESC, name ASC

// Associative array
User::sort(['created_at' => 'desc', 'name' => 'asc'])->get();

// Indexed array
User::sort(['-created_at', 'name'])->get();
```

---

Filter operators
----------------

[](#filter-operators)

OperatorEnum constantValue shapeSQL`exact``FilterOperator::Exact``'foo'` · `'a,b,c'` · `['a','b','c']``= ?` or `IN (...)``like``FilterOperator::Like``'foo'``LIKE '%foo%'``in``FilterOperator::In``'a,b,c'` · `['a','b','c']``IN (...)``between``FilterOperator::Between``['from' => x, 'to' => y]` (each side optional)`BETWEEN` · `>=` · `=` · ` ?``gte``FilterOperator::Gte`scalar`>= ?``lt``FilterOperator::Lt`scalar`< ?``lte``FilterOperator::Lte`scalar` $q->orWhere('name', ...))
        'team.company.name',      // whereHas('team.company', fn($q) => $q->orWhere('name', ...))
    ];
}

public function filterable(): array
{
    return [
        'company.industry'  => FilterOperator::Exact,
        'team.company.name' => FilterOperator::Like,
    ];
}
```

Relation **sorting** is not supported in this version — it requires joins that risk duplicate rows and column-name collisions. Use a raw `orderBy` with an explicit join if you need it.

---

Strict mode
-----------

[](#strict-mode)

By default, an unknown key in `filter()` / `filterHaving()` / `sort()` throws an exception.

ExceptionThrown when`InvalidFilterField`Key not declared in `filterable()` / `having()``InvalidSortField`Field not declared in `sortable()`Disable strict mode to silently skip unknown keys — useful for public APIs where clients may send extra parameters:

```
// config/queryable.php
return ['strict' => false];

// or in .env
QUERYABLE_STRICT=false
```

---

Chaining with native Eloquent
-----------------------------

[](#chaining-with-native-eloquent)

All scopes return the builder, so they compose freely with any Eloquent method:

```
User::query()
    ->whereNotNull('email_verified_at')
    ->with(['company', 'profile'])
    ->search($request->string('q')->toString() ?: null)
    ->filter($request->array('filter'))
    ->sort($request->string('sort')->toString() ?: null)
    ->paginate(25);
```

### HAVING example

[](#having-example)

```
User::query()
    ->withCount('posts')
    ->search($request->string('q')->toString() ?: null)
    ->filter($request->array('filter'))
    ->filterHaving($request->array('having'))
    ->sort($request->string('sort')->toString() ?: null)
    ->paginate(25);
```

---

Full model example
------------------

[](#full-model-example)

```
use Omoba\LaravelQueryable\Concerns\Queryable;
use Omoba\LaravelQueryable\Operators\FilterOperator;

class Transaction extends Model
{
    use Queryable;

    public function searchable(): array
    {
        return ['reference', 'pocket.user.email', 'pocket.user.first_name'];
    }

    public function searchableEncrypted(): array
    {
        return [];
    }

    public function filterable(): array
    {
        return [
            'type'       => FilterOperator::Exact,
            'category'   => FilterOperator::Exact,
            'status'     => FilterOperator::In,
            'created_at' => FilterOperator::DateRange,
            'amount'     => FilterOperator::Gte,
        ];
    }

    public function sortable(): array
    {
        return ['created_at', 'amount'];
    }
}
```

Controller:

```
public function index(Request $request): JsonResponse
{
    $transactions = Transaction::query()
        ->search($request->string('q')->toString() ?: null)
        ->filter($request->array('filter'))
        ->sort($request->string('sort')->toString() ?: null)
        ->with('pocket.user')
        ->paginate($request->integer('per_page', 25));

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

Example requests:

```
# Search across reference and user name/email
GET /transactions?q=john

# Exact filter
GET /transactions?filter[status]=completed

# IN filter (CSV)
GET /transactions?filter[status]=completed,pending

# Date range
GET /transactions?filter[created_at][from]=2026-01-01&filter[created_at][to]=2026-05-25

# Minimum amount
GET /transactions?filter[amount]=10000

# Sort — newest first, then by amount ascending
GET /transactions?sort=-created_at,amount

# Null sentinel
GET /transactions?filter[category]=null

# Combined
GET /transactions?q=jane&filter[status]=completed&filter[created_at][from]=2026-01-01&sort=-created_at
```

---

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

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

Spatie's package is broader in scope (filters, fields, includes, sorts, custom filters). This package takes a different approach:

- **Model-side declaration** — `filterable()`, `sortable()`, `searchable()` live on the model, not in a fluent chain at the call site.
- **First-class `search`** as its own concept, distinct from a filter.
- **No request coupling** — scopes accept plain PHP values, not a `Request` object.

Pick whichever fits your team's mental model.

---

Testing
-------

[](#testing)

```
composer install
composer test      # PHPUnit
composer stan      # PHPStan level 8 + Larastan
vendor/bin/pint --test  # code style check
```

License
-------

[](#license)

MIT

###  Health Score

37

—

LowBetter than 81% of packages

Maintenance98

Actively maintained with recent releases

Popularity2

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity35

Early-stage or recently created project

 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

Unknown

Total

1

Last Release

7d ago

### Community

Maintainers

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

---

Top Contributors

[![omobabello](https://avatars.githubusercontent.com/u/29136907?v=4)](https://github.com/omobabello "omobabello (10 commits)")

---

Tags

searchlaraveleloquentfiltersortquery builder

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/omoba-laravel-queryable/health.svg)

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

###  Alternatives

[kirschbaum-development/eloquent-power-joins

The Laravel magic applied to joins.

1.6k29.9M42](/packages/kirschbaum-development-eloquent-power-joins)[tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models

1.8k5.0M31](/packages/tucker-eric-eloquentfilter)[mohammad-fouladgar/eloquent-builder

526196.6k](/packages/mohammad-fouladgar-eloquent-builder)[mehdi-fathi/eloquent-filter

Eloquent Filter adds custom filters automatically to your Eloquent Models in Laravel.It's easy to use and fully dynamic, just with sending the Query Strings to it.

448196.7k1](/packages/mehdi-fathi-eloquent-filter)

PHPackages © 2026

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