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 Search String Laravel Package

lorisleiva/laravel-search-string

Laravel Search String parses simple, human-friendly search queries like "status:active sort:-created_at name:John" into Eloquent database queries, with support for exact matches, negation, relationships, sorting, and fully customizable syntax.

View on GitHub
Deep Wiki
Context7

Getting Started

composer require lorisleiva/laravel-search-string — the service provider and SearchString facade are auto-discovered, so there is nothing to register manually. As of v1.3.0 the package supports Laravel 10, so it is safe to pull into a Laravel 10 app without a composer.json constraint override.

First three steps:

  1. Publish and skim the config: php artisan vendor:publish --tag=search-string (falls back to --provider="Lorisleiva\LaravelSearchString\LaravelSearchStringServiceProvider"). This file is where case sensitivity, stop words and allowed-field behaviour live — read it before writing any directive.
  2. Add the SearchStringable trait to the model you want to search. That gives you a searchString() scope on the model/query builder.
  3. Try the first use case in tinker before touching a controller:
Post::searchString('laravel author:John -status:trashed')->get();

That single string covers the three things people actually want from this package: a free-text term (laravel), a field constraint (author:John), and a negation (-status:trashed).

The first thing to inspect when behaviour surprises you is the parsed AST, not the SQL — dump the result of SearchString::parse($input) and look at the node tree. Nine times out of ten the parser tokenised your string differently than you assumed (usually a quoting or grouping issue), and the query is fine.

Implementation Patterns

Register directives in a dedicated provider. Keep SearchString::directive(...) calls out of AppServiceProvider::boot(); a small SearchStringServiceProvider (or a Search/ domain folder with a registerDirectives() method) keeps boot time and readability sane once you pass a dozen fields.

SearchString::directive('author', function ($builder, $value) {
    return $builder->whereHas('author', fn ($q) => $q->where('name', 'like', "%{$value}%"));
});

SearchString::directive('status', function ($builder, $value) {
    return $builder->where('status', $value);
});

Route every unscoped term through a default handler. Otherwise bare terms are ignored and the user thinks search is broken. A closure wrapping orWhere in a nested group is the standard shape:

SearchString::default(function ($builder, $value) {
    $builder->where(function ($q) use ($value) {
        $q->where('title', 'like', "%{$value}%")
          ->orWhere('body', 'like', "%{$value}%");
    });
});

Scope the allowed fields per request rather than globally. Public endpoints and admin endpoints rarely share a field set. Build the search string from a whitelist (an array on the controller, a policy, or a config key) and return a 422 for unknown fields — silently ignoring -status:trashed because someone mistyped status is a security-adjacent surprise.

Compose, then apply. Put searchString() last in the chain so it sits alongside your other filters without fighting them:

$posts = Post::query()
    ->where('tenant_id', $tenant->id)
    ->when($request->filled('q'), fn ($q) => $q->searchString($request->string('q')))
    ->paginate();

Case-insensitive search (documented in v1.3.0). On MySQL and SQLite the default collation makes LIKE case-insensitive already; on PostgreSQL it does not. If you need consistent behaviour across engines, either use the package's case-insensitive matching option in the config/Option object, or drop to whereRaw('LOWER(name) LIKE ?', ['%'.strtolower($value).'%']) inside the directive. The v1.3.0 docs exist precisely because people were getting engine-dependent results.

Test directives, not the parser. Pest/PHPUnit datasets of search string => expected SQL or expected model set are cheap and catch regressions when Laravel's query grammar shifts on upgrade.

Gotchas and Tips

  • Negation leaks across OR. -status:trashed must become whereNot/where('status','!=',...) inside a nested closure. If your directive ignores the negation flag, a query with any OR will return the rows you explicitly excluded.
  • Unknown fields fail quietly by default. Decide deliberately: throw, 422, or ignore. Add a test asserting the behaviour so a future refactor doesn't change it silently.
  • Empty or punctuation-only input returns everything. Guard with $request->filled('q') or check the parse result is non-empty before applying — otherwise a stray : or - turns into a full table scan.
  • Quoting. "John Doe" is one value; John Doe is two terms. Document the quoting rules for your users (or your frontend), because this is the number-one support question.
  • Wildcards are not SQL wildcards. A * in a search string does not become %. Translate explicitly in the directive, and never interpolate a value into whereRaw — bind it.
  • Leading-wildcard LIKE kills indexes. %term% on a million-row table will not use a B-tree index. Past a certain size, reach for Scout/Meilisearch and use this package only to build the filter payload.
  • Relationships need whereHas, not where. A directive that does $builder->where('author', $value) on a relation column will error or return nothing; wrap in whereHas and eager-load the relation you filter on to avoid N+1.
  • Config cache in deploy. php artisan config:cache cannot serialise closures. If you put directive callbacks in a config file, they will break in production; keep them in a provider.
  • Debugging order: dump the AST → dump ->toSql() and bindings → only then question the parser. Also check whether another global scope is injected between your directive and execution.
  • Extension point to remember: the config/Option object is where you toggle case sensitivity and relationship support. Changing it globally is tempting and usually wrong — prefer per-controller instances so one noisy endpoint doesn't relax rules for all of them.
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