PHPackages                             shergela/laravel-searchable - 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. shergela/laravel-searchable

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

shergela/laravel-searchable
===========================

A Laravel-specific search/filter package with traits for boolean, date, time, and numeric filters.

v1.7.0(3mo ago)216↓91.7%MITPHPPHP ^8.3

Since Mar 9Pushed 3mo agoCompare

[ Source](https://github.com/SkyWalkerGhost/laravel-searchable)[ Packagist](https://packagist.org/packages/shergela/laravel-searchable)[ RSS](/packages/shergela-laravel-searchable/feed)WikiDiscussions main Synced 3w ago

READMEChangelog (10)Dependencies (4)Versions (23)Used By (0)

Laravel Searchable
==================

[](#laravel-searchable)

`laravel-searchable` is a Laravel package that provides a simple and reusable way to apply **dynamic filtering and searching** to Eloquent queries.

The package includes multiple filter types such as:

- Numeric filters (amount, balance, price)
- Date filters
- Time filters
- Boolean filters
- Ordering helpers
- Full-text search
- Eager loading
- Validation
- Custom methods

Filtering is applied through a **static `Search` builder** that wraps an Eloquent query.

---

Why Use This Package
====================

[](#why-use-this-package)

This package is useful when your application has:

- many filtering conditions
- large search APIs
- reusable filtering logic
- request-based query filtering

It keeps your controllers and services **clean and readable** while keeping filtering logic centralized.

---

Installation
============

[](#installation)

```
composer require shergela/laravel-searchable
```

---

Basic Usage
===========

[](#basic-usage)

The package is used through the `Search` class.

```
use Shergela\Searchable\Search;

$payments = Search::query(Payment::query())
    ->ignoreMissingFields()
    ->with(['payable', 'user'])
    ->id(value: $request->integer('payment_id', null))
    ->userId()
    ->status()
    ->validate()
    ->amount()
    ->orderByDesc();
```

The `Search` builder wraps your Eloquent query and applies filters dynamically.

Validation
==========

[](#validation)

Filtering often uses request values, therefore validation is supported.

Validation can be defined in **two ways**:

1. Using the `Validatable` interface on the model
2. Passing rules manually to the `validate()` method

Using the `Validatable` Interface
=================================

[](#using-the-validatable-interface)

Your model may implement the `Validatable` interface.

```
use Shergela\Searchable\Contracts\Validatable;
```

```
use Illuminate\Database\Eloquent\Model;
use Shergela\Searchable\Contracts\Validatable;

class Payment extends Model implements Validatable
{
    public function rules(): array
    {
        return [
            'id' => ['nullable', 'integer'],
            'user_id' => ['nullable', 'integer'],
            'status' => ['nullable', 'string'],
            'amount' => ['nullable', 'numeric'],
        ];
    }
}
```

When the `validate()` method is called, the package will automatically read these rules.

---

Passing Rules Manually
======================

[](#passing-rules-manually)

If you prefer not to use the interface, you may pass validation rules directly.

```
use Shergela\Searchable\Search;

$payments = Search::query(Payment::query())
    ->validate([
        'amount' => ['nullable', 'numeric'],
        'status' => ['nullable', 'string'],
    ])
    ->amount();
```

Redirect After Validation Failure
=================================

[](#redirect-after-validation-failure)

By default, when validation fails, Laravel redirects back to the previous page. If you need to redirect to a **specific URL** after a failed validation, use the `redirectTo()` method.

```
Search::query(Payment::query())
    ->redirectTo(url: '/payments') // or route('payments.index')
    ->validate([
        'amount' => ['nullable', 'numeric'],
        'status' => ['nullable', 'string'],
    ])
    ->amount()
    ->status()
    ->orderByDesc();
```

> **Recommendation:** Call `redirectTo()` before `validate()` so the redirect destination is defined prior to validation being executed.

---

Important Rule for GET Requests
===============================

[](#important-rule-for-get-requests)

When filtering using **GET requests**, all filter fields **must include the `nullable` rule**.

Example:

```
[
    'amount' => ['nullable', 'numeric'],
]
```

This is important because when the field is not present in the request, Laravel validation would otherwise fail.

---

Filtering Methods
=================

[](#filtering-methods)

Every filter method supports **three ways of receiving its value**. This allows the filter to work with requests, manual values, or custom input names.

---

1. Automatic Value Detection from Field
=======================================

[](#1-automatic-value-detection-from-field)

Filters will now automatically retrieve the value based on the field name. You no longer need to pass the request manually.

```
Search::query(Payment::query())
    ->amount(); // the field name is 'amount'
```

Example request:

```
GET /payments?amount=100

```

SQL equivalent:

```
WHERE amount = 100

```

---

2. Passing a Custom Field Name
==============================

[](#2-passing-a-custom-field-name)

Each filter method has a default field name it uses to read the value from the request and the corresponding database column. If your request input name or database column differs from the default, you can pass a custom field.

> **Important:** The `field` value must match **both** the request input name and the database column name — they must be identical.

```
Search::query(Payment::query())
    ->amount(field: 'total_amount');
```

Example request:

```
GET /payments?total_amount=100

```

SQL equivalent:

```
WHERE total_amount = 100

```

---

3. Passing Field + Value
========================

[](#3-passing-field--value)

You may bypass the request completely.

```
Search::query(Payment::query())
    ->amount(field: 'amount', value: 100);
```

This is useful when filtering from:

- DTOs
- services
- computed values
- CLI inputs

---

Ignoring Missing Fields
=======================

[](#ignoring-missing-fields)

When a form has **disabled input fields**, the browser does not include them in the request payload. This means the corresponding key will be entirely absent from the request, which can cause the filter to behave unexpectedly — for example, applying an empty condition to the query.

Calling `ignoreMissingFields()` instructs the package to **skip any filter whose field is not present in the request**, preventing empty or unnecessary query conditions from being applied.

> **Recommendation:** Always call `ignoreMissingFields()` at the **beginning of the chain**, before any filter methods, to ensure consistent behavior across all filters.

```
Search::query(Payment::query())
    ->ignoreMissingFields()  // status()
    ->amount()
    ->orderByDesc();
```

Without `ignoreMissingFields()`, a disabled or absent field could still be evaluated and result in an unintended `WHERE` clause.

---

Full Text Search
================

[](#full-text-search)

The `fullTextSearch()` method performs an optimized text search across one or more columns. It automatically detects the database driver and applies the most appropriate search strategy.

Supported Drivers
-----------------

[](#supported-drivers)

DriverStrategy`pgsql``tsvector` + `plainto_tsquery``mysql``MATCH ... AGAINST` (Natural Language Mode)Other (SQLite, etc.)`LIKE` fallback with `LOWER()`Parameters
----------

[](#parameters)

ParameterTypeDefaultDescription`$columns``array``['full_name']`Columns to search in (max 5)`$relation``string|null``null`Relation name if searching in a related model`$value``string|null``null`Search value. If `null`, filter is skippedBasic Usage
-----------

[](#basic-usage-1)

Search within the model's own columns:

```
Search::query(Payment::query())
    ->fullTextSearch(
        columns: ['first_name', 'last_name'],
        value: $request->string('name')->value()
    );
```

Searching Within a Relation
---------------------------

[](#searching-within-a-relation)

If the searchable columns belong to a related model, pass the relation name:

```
Search::query(Payment::query())
    ->fullTextSearch(
        columns: ['first_name', 'last_name'],
        relation: 'user',
        value: $request->string('user_name')->value()
    );
```

This translates to:

```
WHERE EXISTS (
    SELECT * FROM users
    WHERE payments.user_id = users.id
    AND MATCH(first_name, last_name) AGAINST(? IN NATURAL LANGUAGE MODE)
)
```

Driver Behavior
---------------

[](#driver-behavior)

**PostgreSQL:**

```
WHERE (to_tsvector('simple', coalesce("first_name", '')) || to_tsvector('simple', coalesce("last_name", '')))
    @@ plainto_tsquery('simple', ?)
```

**MySQL:**

```
WHERE MATCH(`first_name`, `last_name`) AGAINST(? IN NATURAL LANGUAGE MODE)
```

**SQLite / Other (LIKE fallback):**

```
WHERE LOWER("first_name") LIKE '%john%'
   OR LOWER("last_name") LIKE '%john%'
```

Validation Rules
----------------

[](#validation-rules)

The method enforces two rules on the `columns` array:

- Maximum **5 columns** allowed
- Column names must be **valid identifiers** — only letters, numbers, underscores, and dots are permitted (e.g. `first_name`, `address.city`). SQL injection attempts are rejected.

```
// ✅ Valid
->fullTextSearch(columns: ['first_name', 'last_name', 'email'], value: 'john')

// ❌ Throws InvalidArgumentException — too many columns
->fullTextSearch(columns: ['a', 'b', 'c', 'd', 'e', 'f'], value: 'john')

// ❌ Throws InvalidArgumentException — invalid column name
->fullTextSearch(columns: ['first_name; DROP TABLE users'], value: 'john')
```

Notes
-----

[](#notes)

- If `$value` is `null`, the filter is **silently skipped** — no query condition is added.
- The `LIKE` fallback is **case-insensitive** via `LOWER()`.
- For **MySQL**, ensure the columns have a `FULLTEXT` index for optimal performance.
- For **PostgreSQL**, the `'simple'` dictionary is used, meaning no language-specific stemming is applied.

---

Eager Loading
=============

[](#eager-loading)

Since the package wraps an Eloquent builder, you may also use `with()`.

```
Search::query(Payment::query())
    ->with(['user', 'payable']);
```

---

Available Ordering/Helper Methods
=================================

[](#available-orderinghelper-methods)

### methods:

[](#methods)

- get
- first
- pluck
- orderBy
- orderByDesc
- latest
- paginate
- cursorPaginate

```
Search::query(Payment::query())
    ->orderByDesc()
    ->get();
```

Example SQL:

```
ORDER BY id DESC

```

---

Custom Methods
==============

[](#custom-methods)

If the built-in filter methods are not enough, you can extend the `Searchable` class and define your own custom methods.

Creating a Custom Service
-------------------------

[](#creating-a-custom-service)

Extend `Shergela\Searchable\Searchable` and implement the `model()` method to return the base Eloquent query. Then define your custom filter methods using the internal `search()` helper.

```
use Illuminate\Database\Eloquent\Builder;
use Shergela\Searchable\Searchable;

class UserService extends Searchable
{
    protected function model(): Builder
    {
        return User::query();
    }

    public function customMethod(string $field = 'id', ?string $value = null, string $operator = '='): static
    {
        $this->search(field: $field, operator: $operator, value: $value);

        return $this;
    }

    public function customMethod2(string $field = 'id', ?string $value = null, string $operator = '='): static
    {
        $this->search(field: $field, operator: $operator, value: $value);

        return $this;
    }
}
```

Calling Custom Methods
----------------------

[](#calling-custom-methods)

Use the static `query()` entry point, just like with the `Search` class.

```
UserService::query()
    ->customMethod(field: 'status', value: $request->string('status')->value())
    ->customMethod2(field: 'email', value: $request->string('email')->value())
    ->orderByDesc();
```

This approach keeps filtering logic **encapsulated in a dedicated service class**, making it easy to reuse and test.

---

Using Laravel's Eloquent Methods Directly
=========================================

[](#using-laravels-eloquent-methods-directly)

If you need to use native Laravel Eloquent methods (such as `where`, `select`, `join`, `paginate`, etc.), you can access the underlying Eloquent builder via the `builder()` method.

```
UserService::query()
    ->customMethod(field: 'status', value: $request->string('status')->value())
    ->builder()
    ->where('verified', true)
    ->paginate(15);
```

> **Recommendation:** Call `builder()` at the **end of the chain**, after all package methods have been applied. Calling it earlier will return a plain Eloquent builder, meaning you will lose access to the package's custom methods for any subsequent calls.

---

Full Example
============

[](#full-example)

```
use Shergela\Searchable\Search;

$payments = Search::query(Payment::query())
    ->ignoreMissingFields()
    ->with(['payable', 'user'])
    ->id(value: $request->integer('payment_id', null))
    ->userId()
    ->status()
    ->validate()
    ->amount()
    ->orderByDesc();
```

License
=======

[](#license)

MIT

###  Health Score

42

—

FairBetter than 89% of packages

Maintenance82

Actively maintained with recent releases

Popularity9

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity59

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

Total

22

Last Release

95d ago

PHP version history (2 changes)v1.0.0PHP ^8.2

v1.2.0PHP ^8.3

### Community

Maintainers

![](https://www.gravatar.com/avatar/1d2fffa3fad8bf3cc7b57dcc354ed664558c869668b4bd51ec4b8fba3ca6f5ac?d=identicon)[SKEDROW](/maintainers/SKEDROW)

---

Top Contributors

[![SkyWalkerGhost](https://avatars.githubusercontent.com/u/39098776?v=4)](https://github.com/SkyWalkerGhost "SkyWalkerGhost (24 commits)")

###  Code Quality

TestsPHPUnit

Code StyleLaravel Pint

### Embed Badge

![Health badge](/badges/shergela-laravel-searchable/health.svg)

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

###  Alternatives

[ircmaxell/filterus

A library for filtering variables in PHP

44613.4k6](/packages/ircmaxell-filterus)[awesome-nova/dependent-filter

Dependent filters for Laravel Nova

26190.2k](/packages/awesome-nova-dependent-filter)

PHPackages © 2026

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