Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Searchzy Laravel Package

jhormantasayco/laravel-searchzy

Laravel Searchzy is a lightweight package that adds simple, chainable search to your Eloquent models. Filter records by multiple columns, handle relationships and custom queries, and keep your controllers clean without pulling in a full search engine.

View on GitHub
Deep Wiki
Context7

Getting Started

Install it like any other small utility package:

composer require jhormantasayco/laravel-searchzy

The service provider is auto-discovered on the Laravel versions the package targeted (roughly 7/8). Before you run this in a modern app, sanity-check the dependency graph — the last release was Feb 2021, so composer why-not jhormantasayco/laravel-searchzy is your first command if install fails on Laravel 9+.

The public surface is intentionally tiny: add the trait to the model you want to filter, declare which columns are searchable, and call the scope from your controller.

namespace App\Models;

use Illuminate\Database\Eloquent\Model;
use Jhormantasayco\LaravelSearchzy\Traits\Searchzy;

class Post extends Model
{
    use Searchzy;

    // column => operator. This is your whitelist.
    protected $searchable = [
        'title'   => 'like',
        'body'    => 'like',
        'status'  => '=',
        'user_id' => '=',
    ];
}

Your first real use case will almost always be an index page with a search box plus a couple of dropdown filters:

public function index(Request $request)
{
    $posts = Post::searchzy($request->only(['q', 'status', 'user_id']))
        ->latest()
        ->paginate(20)
        ->withQueryString();

    return view('posts.index', compact('posts'));
}

Where to look first, in order: the $searchable examples in the README, the trait source (vendor/jhormantasayco/laravel-searchzy/src), and then the generated SQL. That last step matters more than the docs — the package is small enough to read end-to-end in ten minutes, and you will need to know exactly how it groups conditions.

Implementation Patterns

Validate the filter keys, not just the values. The scope consumes an array keyed by column name. Treat that array as untrusted input and shape it in a FormRequest or a dedicated PostFilters object before it reaches Eloquent:

$filters = $request->validate([
    'q'       => ['nullable', 'string', 'max:100'],
    'status'  => ['nullable', Rule::in(['draft', 'published'])],
    'user_id' => ['nullable', 'integer', 'exists:users,id'],
]);

$posts = Post::searchzy(array_filter($filters))->paginate(20);

Keep the scope in the model, keep the policy in the controller. Combine it with authorization and tenant scoping rather than replacing them:

$posts = Post::query()
    ->where('team_id', $request->user()->team_id)  // tenancy first
    ->searchzy($filters)                            // then user filters
    ->with('author:id,name')                        // eager load what you render
    ->paginate(20);

Mirror the same form between web and API. Because the scope takes a plain array, both a Blade form POST and a JSON GET /api/posts?q=... can feed the same filter object. Build one PostFilters::fromRequest() and reuse it.

Blade partial for the search form. Preserve submitted values so the box doesn't clear on pagination:

<form method="GET" action="{{ route('posts.index') }}">
    <input name="q" value="{{ request('q') }}" placeholder="Search posts...">
    <select name="status">
        <option value="">All</option>
        @foreach (['draft', 'published'] as $s)
            <option value="{{ $s }}" @selected(request('status') === $s)>{{ $s }}</option>
        @endforeach
    </select>
    <button type="submit">Filter</button>
</form>

Wrap it in a local scope for team conventions. If you standardise on q for the free-text term, translate once instead of in every controller:

public function scopeFiltered(Builder $query, array $input): Builder
{
    return $query->searchzy([
        'q'      => $input['q'] ?? null,
        'status' => $input['status'] ?? null,
    ]);
}

Test the SQL, not the package. Assert that a filtered request returns the right rows and that an unauthorised column in the input is ignored — that is the behaviour you actually care about.

Gotchas and Tips

  • Column injection is the real risk. If the package maps request keys directly to columns without checking $searchable, any key the user sends becomes a WHERE. Always verify $searchable is enforced; if it isn't, whitelist in your controller with array_intersect_key($input, array_flip(['q', 'status', 'user_id'])).
  • LIKE with empty input. An empty string often becomes LIKE '%%', which is a no-op but still a scan. Strip nulls and empty strings before calling the scope (array_filter($filters, 'filled')).
  • Wildcards in user input. % and _ typed by a user are treated as SQL wildcards. If that matters, escape them or switch the column to = and use a separate prefix search.
  • Case sensitivity differs by driver. MySQL's default collation makes LIKE case-insensitive; Postgres does not. Don't assume parity between local SQLite and production Postgres when writing tests.
  • orWhere grouping. Simple filter packages frequently emit WHERE a = ? OR b = ? without wrapping parentheses, which breaks the moment you add a where('team_id', ...) alongside it. Always ->toSql() a combined query once and read it.
  • No relation filtering unless documented. Filtering on author.name via dot notation is a common expectation and a common absence. If relations aren't supported, use whereHas manually rather than fighting the scope.
  • N+1 on results pages. The scope handles filtering only; it will not eager-load. Pair it with with() for anything you render in a loop.
  • Index for it. LIKE '%term%' cannot use a B-tree index. For tables beyond a few thousand rows, plan on a FULLTEXT/tsvector index or a dedicated search driver instead of stretching this package.
  • Maintenance is the biggest gotcha. 14 stars, last release 2021, no evidence of Laravel 10/11 support. Pin it, read the source, and keep a fallback in mind: spatie/laravel-query-builder for richer filtering, or a 40-line local scope if your needs are as simple as they look. The trait is small enough to vendor into app/Support if the package becomes uninstallable.
  • Extension points are limited. Because there is no config file or driver abstraction, customisation means overriding the scope in your model or wrapping it. That's fine — it's a convenience layer, not a query language.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core
nodisrupt/laravel-agent
sierratecnologia/media-manager