PHPackages                             firevel/filterable - 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. firevel/filterable

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

firevel/filterable
==================

A simple trait for Laravel Eloquent models that allows you to easily filter your queries.

0.0.16(4mo ago)05.4k↓50%2[2 PRs](https://github.com/firevel/filterable/pulls)1MITPHP

Since Oct 11Pushed 4mo agoCompare

[ Source](https://github.com/firevel/filterable)[ Packagist](https://packagist.org/packages/firevel/filterable)[ RSS](/packages/firevel-filterable/feed)WikiDiscussions main Synced 1mo ago

READMEChangelog (10)DependenciesVersions (19)Used By (1)

Laravel Filterable
==================

[](#laravel-filterable)

A lightweight trait for Laravel Eloquent models that makes it easy to build dynamic, type‐safe filters on your queries. Instead of hard‐coding dozens of scopes or query clauses, simply declare which fields are “filterable” and let the trait handle operators, casting, and relationship logic for you.

---

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

[](#table-of-contents)

1. [Installation](#installation)
2. [Quick Start](#quick-start)
3. [Configuration](#configuration)
    - [Defining `$filterable`](#defining-filterable)
    - [Allowed Filter Types](#allowed-filter-types)
    - [Supported Operators](#supported-operators)
    - [Validating Columns](#validating-columns)
4. [Basic Usage](#basic-usage)
    - [Filtering by Single Field](#filtering-by-single-field)
    - [Filtering by Multiple Fields](#filtering-by-multiple-fields)
    - [Composite (“Virtual”) Filters](#composite-virtual-filters)
5. [Advanced Filters](#advanced-filters)
    - [Filtering JSON Columns](#filtering-json-columns)
    - [Filtering Relationships](#filtering-relationships)
    - [Boolean &amp; Null Checks](#boolean--null-checks)
6. [Examples](#examples)
7. [Tips &amp; Best Practices](#tips--best-practices)
8. [Troubleshooting](#troubleshooting)

---

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

[](#installation)

Install via Composer:

```
composer require firevel/filterable

```

Once installed, there are no service‐provider registrations or config publishes required. The trait is ready to use.

---

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

[](#quick-start)

1. Add the `Filterable` trait to your Eloquent model.
2. Define a protected `$filterable` array, mapping each filter key to its type.
3. Call the `filter([...])` scope on your queries.

```
// In app/Models/User.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Firevel\Filterable\Filterable;

class User extends Model
{
    use Filterable;

    /**
     * Specify which fields (or “virtual” keys) can be filtered,
     * along with their data types.
     */
    protected $filterable = [
        'id'         => 'id',
        'first_name' => 'string',
        'last_name'  => 'string',
        'email'      => 'string',
        'created_at' => 'datetime',
    ];
}

```

Now you can do:

```
$users = User::filter([
    'first_name' => ['like' => 'Smith'],
    'created_at' => ['>'    => '2023-01-01'],
])->get();

```

––

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

[](#configuration)

### Defining `$filterable`

[](#defining-filterable)

In each model that uses the trait, declare a protected `$filterable` array. The **keys** are the names (or aliases) you wish to filter on, and the **values** specify the field’s type. For example:

```
protected $filterable = [
    'id'            => 'id',
    'first_name'    => 'string',
    'last_name'     => 'string',
    'email'         => 'string',
    'created_at'    => 'datetime',
    'is_active'     => 'boolean',
    'meta'          => 'json',
    'roles'         => 'relationship',
];

```

- If a key corresponds to an actual database column, use its column name.
- If you want “virtual” filters (e.g. `full_name` that searches both `first_name` and `last_name`), see the [Composite (“Virtual”) Filters](#composite-virtual-filters) section.

The trait will only apply filters for keys explicitly declared in `$filterable`; any others are ignored by default (or throw an exception if you enable column validation).

---

### Allowed Filter Types

[](#allowed-filter-types)

TypeDescription`integer`Integer columns or numeric IDs`id`Shorthand for integer when representing a primary/foreign key`string`Text columns; used with operators like `like`, `=`, ```date`Date‐only filters (YYYY‐MM‐DD). Under the hood, uses `whereDate()``datetime`Date &amp; time filters (YYYY‐MM‐DD HH:MM:SS). Uses `whereDate()` if value is 10 chars long`boolean`Casts “true”/“false” (case‐insensitive) to boolean`json`JSON columns; used with `where()` or JSON operators`array`JSON columns containing arrays; uses `whereJsonContains()``relationship`Expect a related model or “has” filter on a `belongsTo` / `hasMany` style relationship---

### Supported Operators

[](#supported-operators)

By default, the trait allows the following operators for each filter type. To override operators on a field, simply pass an associative array (`'[ operator ] => [ value ]'`).

OperatorAliasMeaningAllowed Types`=``eq`Equal to (default if no operator provided)`integer`, `id`, `string`, `date`, `datetime`, `relationship`, `boolean`, `json`, `array````ne`Not equal to`integer`, `id`, `string``>``gt`Greater than`integer`, `date`, `datetime`, `id`, `relationship``>=``gte`Greater than or equal`integer`, `date`, `datetime`, `id`, `relationship`` 'foo']`, the trait assumes the `=` operator by default.

#### Operator Aliases

[](#operator-aliases)

To avoid using special characters in URLs, you can use text-based aliases for comparison operators:

- `gt` for `>` (greater than)
- `gte` for `>=` (greater than or equal)
- `lt` for ` 25] ])->get();

// Using alias operators (URL-friendly)
$users = User::filter([ 'age' => ['gt' => 25] ])->get();

// Both produce: SELECT * FROM users WHERE age > 25

```

---

### Validating Columns

[](#validating-columns)

By default, the trait will **ignore** any filters whose key is not in `$filterable`. If you’d rather throw an exception when an unknown filter is passed, enable column validation:

```
class User extends Model
{
    use Filterable;

    protected $validateColumns = true;

    protected $filterable = [
        'id'       => 'id',
        'email'    => 'string',
        'status'   => 'string',
    ];
}

```

With `$validateColumns = true`, passing `->filter(['not_a_column' => ['=' => 5]])` will throw:

```
Exception: Filter column 'not_a_column' is not allowed.

```

---

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

[](#basic-usage)

### Filtering by Single Field

[](#filtering-by-single-field)

Filter on one attribute by providing a key‐value pair. If you omit the operator, it defaults to `=`.

```
// 1) Simple equality (defaults to '=')
$users = User::filter([ 'id' => 5 ])->get();
// → SELECT * FROM users WHERE id = 5;

// 2) Explicit operators
$users = User::filter([ 'created_at' => ['>' => '2024-01-01'] ])->get();
// → SELECT * FROM users WHERE created_at > '2024-01-01';

// 3) LIKE operator for strings
$users = User::filter([ 'email' => ['like' => '%@example.com'] ])->get();
// → SELECT * FROM users WHERE email LIKE '%@example.com';

```

### Filtering by Multiple Fields

[](#filtering-by-multiple-fields)

Combine as many filters as you need; they are joined with `AND` logic:

```
$filters = [
    'first_name' => ['like' => 'John'],
    'created_at' => ['>='   => '2025-01-01'],
    'status'     => ['='    => 'active'],
];

$users = User::filter($filters)->get();
// → SELECT * FROM users
//    WHERE first_name LIKE '%John%'
//      AND created_at >= '2025-01-01'
//      AND status = 'active';

```

---

### Composite (“Virtual”) Filters

[](#composite-virtual-filters)

Sometimes you want a single filter key (e.g. `name`) that actually applies to multiple columns (like `first_name` **OR** `last_name`). You can achieve this by declaring a “scope”-type entry in `$filterable` and then adding a local scope method on your model.

#### Example: “name” → searches `first_name` OR `last_name`

[](#example-name--searches-first_name-or-last_name)

1. **Declare a `scope` filter key**
    In `User.php`:

    ```
    use Illuminate\Database\Eloquent\Model;
    use Firevel\Filterable\Filterable;

    class User extends Model
    {
        use Filterable;

        protected $filterable = [
            'first_name' => 'string',
            'last_name'  => 'string',
            'email'      => 'string',
            'created_at' => 'datetime',

            // “name” isn’t a real column; mark it as a custom scope
            'name'       => 'scope',
        ];

        // Add a local scopeName() to combine first_name OR last_name
        // The second parameter ($allFilters) provides access to all filters
        public function scopeName($query, $value, $allFilters = [])
        {
            $query->where(function ($q) use ($value) {
                $q->where('first_name', 'like', "%{$value}%")
                  ->orWhere('last_name',  'like', "%{$value}%");
            });
        }
    }

    ```
2. **Use it in your code exactly like any other filter**

    ```
    // Will invoke scopeName() internally
    $users = User::filter([
        'name'       => ['like' => 'Smith'],
        'created_at' => ['>'    => '2025-01-01']
    ])->get();

    ```

    Under the hood, the trait sees `'name' => 'scope'` and calls `$query->name('Smith')`, which in turn applies:

    ```
    WHERE (first_name LIKE '%Smith%' OR last_name LIKE '%Smith%')
      AND created_at > '2025-01-01'

    ```

#### Why use a "scope"-type filter?

[](#why-use-a-scope-type-filter)

- **Zero changes to the trait**: the existing code already checks `if ($filterType === 'scope')` and executes the corresponding local scope.
- **Keeps your trait logic simple**: you don't have to override the trait's internal validation or operator parsing—your `scopeName()` takes full responsibility for how the filter behaves.
- **Reusable &amp; readable**: everyone knows that "scopeX" is a local query modifier, and the trait simply defers to it.

#### Scope Method Naming Convention

[](#scope-method-naming-convention)

The trait supports two naming conventions for scope methods and uses Laravel's `Str::studly()` for conversion:

1. **Prefixed (recommended)**: `scopeFilter{Name}` — e.g., `scopeFilterSearch` for filter key `search`
2. **Simple**: `scope{Name}` — e.g., `scopeSearch` for filter key `search`

The prefixed convention is checked first and is recommended to avoid conflicts with reserved Laravel method names like `query`, `where`, `get`, etc.

```
// Recommended: use scopeFilter prefix to avoid conflicts
protected $filterable = [
    'query' => 'scope',  // Safe! Won't conflict with $query->query()
];

public function scopeFilterQuery($query, $value, $allFilters = [])
{
    // Your custom filter logic
}

// Also supported for backward compatibility
protected $filterable = [
    'name' => 'scope',
];

public function scopeName($query, $value, $allFilters = [])
{
    // Your custom filter logic
}

```

Filter names with underscores or dots are converted using StudlyCase:

Filter KeyPrefixed MethodSimple Method`search``scopeFilterSearch``scopeSearch``user_status``scopeFilterUserStatus``scopeUserStatus``user.role``scopeFilterUserRole``scopeUserRole`#### Accessing Other Filters in Scope Methods

[](#accessing-other-filters-in-scope-methods)

Scope filter methods receive two parameters:

1. **`$value`** - The specific value for this filter
2. **`$allFilters`** - The complete array of all filters being applied

This allows you to create conditional logic based on other filters:

```
protected $filterable = [
    'search'     => 'scope',
    'category'   => 'string',
    'status'     => 'string',
];

public function scopeSearch($query, $value, $allFilters = [])
{
    $query->where(function ($q) use ($value, $allFilters) {
        $q->where('title', 'like', "%{$value}%")
          ->orWhere('description', 'like', "%{$value}%");

        // Apply different search logic if category filter is present
        if (isset($allFilters['category'])) {
            $q->orWhere('tags', 'like', "%{$value}%");
        }
    });
}

```

---

Advanced Filters
----------------

[](#advanced-filters)

### Filtering JSON Columns

[](#filtering-json-columns)

If you have a JSON column (e.g. `meta`), you can:

- **Filter by exact JSON key‐value**:

    ```
    protected $filterable = [
        'meta' => 'json',
        // … other fields …
    ];

    ```

    ```
    // Get users whose JSON “meta->role” equals “admin”
    $users = User::filter([ 'meta->role' => ['=' => 'admin'] ])->get();
    // → SELECT * FROM users WHERE JSON_EXTRACT(meta, '$.role') = 'admin';

    ```
- **Filter by array contents** (for JSON arrays) by using type `array`:

    ```
    protected $filterable = [
        'tags' => 'array', // assumes tags is a JSON array column
    ];

    ```

    ```
    // Check if JSON array contains a value using 'in' operator
    $users = User::filter([ 'tags' => ['in' => 'premium'] ])->get();
    // → SELECT * FROM users WHERE JSON_CONTAINS(tags, '"premium"');

    // Check if JSON array contains ANY of multiple values
    $users = User::filter([ 'tags' => ['in' => 'premium,vip'] ])->get();
    // → SELECT * FROM users WHERE (JSON_CONTAINS(tags, '"premium"') OR JSON_CONTAINS(tags, '"vip"'));

    // Exact match on the entire array using '=' operator
    $users = User::filter([ 'tags' => ['=' => '["premium","vip"]'] ])->get();
    // → SELECT * FROM users WHERE tags = '["premium","vip"]';

    ```

---

### Filtering Relationships

[](#filtering-relationships)

If you want to filter on related models (e.g. `User` hasMany `Order`), declare the key as `relationship` in `$filterable`. Then pass either:

1. A scalar/array (for simple `has()` checks).
2. A nested filter array to apply conditions on the related model.

```
// In User.php
protected $filterable = [
    'email'      => 'string',
    'orders'     => 'relationship',
];

// In Order.php (no special setup required)
class Order extends Model { /* … */ }

```

```
// 1) Just check that a user has at least one order:
$usersWithAnyOrder = User::filter([ 'orders' => ['>' => 0] ])->get();
// → SELECT * FROM users
//    WHERE ( SELECT COUNT(*) FROM orders WHERE orders.user_id = users.id ) > 0;

// 2) Filter by a condition on the order itself:
$filters = [
    'orders.status' => ['=' => 'shipped'],
    'email'         => ['like' => '%@example.com'],
];
$users = User::filter($filters)->get();
// → SELECT * FROM users
//    WHERE EXISTS (
//      SELECT 1 FROM orders
//       WHERE orders.user_id = users.id
//         AND status = 'shipped'
//    )
//      AND email LIKE '%@example.com';

```

> **Tip:** If you need a more complex subquery on the relationship, you can chain `useRelationshipQuery()` before calling `filter()`.
>
> ```
> // Define a custom where clause for the related model
> $relatedWhere = function ($query) {
>     $query->where('price', '>', 100);
> };
>
> User::useRelationshipQuery($relatedWhere)
>     ->filter([ 'orders' => ['in' => [1,2,3]] ])
>     ->get();
>
> ```

---

### Boolean &amp; Null Checks

[](#boolean--null-checks)

- **Boolean**

    ```
    protected $filterable = [
        'is_active' => 'boolean',
    ];

    ```

    ```
    // Accepts true/false, "1"/"0", "true"/"false" (case insensitive)
    $activeUsers   = User::filter(['is_active' => ['=' => 'true']])->get();
    $inactiveUsers = User::filter(['is_active' => ['=' => '0']])->get();

    ```
- **`IS NULL` / `IS NOT NULL`**
    For any type (`integer`, `string`, `date`, etc.), you can check nulls via `is` or `not` with the literal `'null'`:

    ```
    // Users with no email
    $usersNoEmail = User::filter([ 'email' => ['is' => 'null'] ])->get();

    // Users where deleted_at IS NOT NULL (soft‐deleted)
    $trashed = User::filter([ 'deleted_at' => ['not' => 'null'] ])->get();

    ```

---

Examples
--------

[](#examples)

Below are a few real‐world scenarios illustrating how you might combine filters.

```
// 1) Find all “admin” users created in the last 30 days,
//    whose email domain is “example.com” and have placed at least one “shipped” order.

$filters = [
    'role'       => ['='    => 'admin'],
    'created_at' => ['>='   => now()->subDays(30)->toDateString()],
    'email'      => ['like' => '%@example.com'],
    'orders.status' => ['=' => 'shipped'],
];

$admins = User::filter($filters)
    ->orderBy('created_at', 'desc')
    ->paginate(15);

// 2) Search by “full name” (composite filter: first_name OR last_name),
//    and also filter by a JSON metadata key:
$filters = [
    'name'           => ['like' => 'Doe'],         // see “Composite Filters”
    'meta->department'=> ['='  => 'engineering'],  // JSON column
    'status'         => ['='    => 'active'],
];

$users = User::filter($filters)->get();

// 3) Get all products whose "tags" JSON array includes "sale" or "new":
$filters = [
    'tags' => ['in' => 'sale,new'],  // checks if array contains any of these values
];

$productsOnSaleOrNew = Product::filter($filters)->get();

```

---

Tips &amp; Best Practices
-------------------------

[](#tips--best-practices)

- **Keep `$filterable` up to date**: Every column or relationship you wish to filter on must appear in the array.
- **Use strict column validation in production**: ```
    protected $validateColumns = true;

    ```

    This prevents typos or malicious filters from silently being ignored.
- **Leverage composite (virtual) filters sparingly**: Only create a custom scope if you truly need to combine two or more columns into one semantic filter.
- **Avoid leading wildcards unless necessary**:
    - `LIKE '%foo%'` is flexible but slow on large tables. Whenever possible, use `LIKE 'foo%'` or full‐text search.
- **Paginate filtered results**: Filtering can return large result sets. Always pair with `→paginate()` or `→simplePaginate()` to avoid memory issues.
- **Test your JSON and relationship filters** thoroughly—wrong syntax or missing indexes can lead to unexpected results or performance hits.

---

Troubleshooting
---------------

[](#troubleshooting)

- **“Filter column ‘xyz’ is not allowed.”**
    You enabled `protected $validateColumns = true` and passed a key not in `$filterable`. Either add it to the array or disable validation.
- **`Operator ‘in’ is not allowed for type ‘integer’`**
    Check your `$filterable` type for that key. The `in` operator only works on `integer`, `id`, `string`, or `json`—not on `date/datetime` out of the box.
- **Composite filter not working**
    If you declared a key as `'scope'` in `$filterable` (for example, `'name' => 'scope'`), make sure you have a corresponding `scopeName()` method on the model. If the trait can’t find `scopeName`, it will skip your filter.
- **Slow queries on large tables**

    - Check if you’re using `%…%` wildcards (leading `%`) on very large text columns—those can’t use indexes.
    - Consider adding a full‐text index for complex search scenarios or switch to a dedicated search engine (Scout, Algolia, MeiliSearch).

---

With this simple trait, you can keep your controllers and repositories neat, DRY, and expressive—no more copy/pasting dozens of `if ($request->has('…')) { … }` checks. Happy filtering!

###  Health Score

41

—

FairBetter than 89% of packages

Maintenance75

Regular maintenance activity

Popularity25

Limited adoption so far

Community13

Small or concentrated contributor base

Maturity41

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 87.5% 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 ~54 days

Recently: every ~36 days

Total

16

Last Release

136d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/3ef4a79c6f9a9afe04267a19b98fe0a5a45930c92d08fd720b233ab21ae102ca?d=identicon)[sl0wik](/maintainers/sl0wik)

---

Top Contributors

[![sl0wik](https://avatars.githubusercontent.com/u/2696038?v=4)](https://github.com/sl0wik "sl0wik (28 commits)")[![mphaedok](https://avatars.githubusercontent.com/u/40614383?v=4)](https://github.com/mphaedok "mphaedok (2 commits)")[![Phaedok](https://avatars.githubusercontent.com/u/6504318?v=4)](https://github.com/Phaedok "Phaedok (2 commits)")

---

Tags

laraveltraitfiltersfilterablefirevel

### Embed Badge

![Health badge](/badges/firevel-filterable/health.svg)

```
[![Health](https://phpackages.com/badges/firevel-filterable/health.svg)](https://phpackages.com/packages/firevel-filterable)
```

###  Alternatives

[millat/laravel-hooks

The WordPress filter, action system in Laravel

5715.1k](/packages/millat-laravel-hooks)[kirschbaum-development/livewire-filters

Livewire Filters is a series of Livewire components that provide you with the tools to do live filtering of your data from your own Livewire components.

164.1k](/packages/kirschbaum-development-livewire-filters)[mobileka/scope-applicator

Scope Applicator is a PHP trait that makes data filtering and sorting easy.

251.2k2](/packages/mobileka-scope-applicator)

PHPackages © 2026

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