faithfm/smart-search-php
faithfm/smart-search-php is a Laravel/PHP package that adds smarter, more flexible search to your applications. It helps turn simple user input into useful queries, making list pages and search results easier to build and more relevant for users.
Assessment reflects release 2.3.0. The package exposes an Eloquent SmartSearchable trait plus a SmartSearch service class; the details below assume that shape and should be checked against the trait source on first install.
Install and wire up
composer require <vendor>/<package>
php artisan vendor:publish --tag=<package>-config # if a config file ships
Add the trait to any model you want searchable:
use Vendor\Package\Traits\SmartSearchable;
class Post extends Model
{
use SmartSearchable;
protected array $searchable = ['title', 'body', 'author.name'];
}
Where to look first
src/Traits/SmartSearchable.php — the scope definitions are the real API surface (smartSearch, and as of 2.3.0 smartSearchWithErrors).src/SmartSearch.php — term parsing, column resolution, and where "errors" actually originate.First use case
A search box that filters an index page:
// app/Http/Controllers/PostController.php
public function index(Request $request)
{
$posts = Post::query()
->smartSearch($request->string('q'))
->latest()
->paginate(20);
return view('posts.index', compact('posts'));
}
That is the whole happy path. The 2.3.0 addition matters the moment you need to tell the user why a query behaved unexpectedly (ignored terms, unparsable operators, unknown column references) rather than silently returning partial results.
Basic scope chaining. smartSearch() behaves like any other query scope, so keep it early and compose freely:
Post::query()
->where('published', true)
->smartSearch($term)
->with('author')
->paginate(20);
Surfacing search problems to the UI (new in 2.3.0). The intended pattern is a second scope that gives you the same builder and access to the errors collected during term parsing:
$query = Post::smartSearchWithErrors($request->string('q'));
$posts = $query->paginate(20);
$errors = Post::searchErrors(); // verify the exact accessor name in the trait
return view('posts.search', compact('posts', 'errors'));
Use this where a search box is user-facing and an empty result could mean "no matches" or "your query was garbage". The plain smartSearch() scope stays the right choice for internal/admin tooling where you only care about results.
Validate before you search, not after. Treat the scope as a query builder, not a validator. Guard the term with a FormRequest (length limits, allowed characters) and use smartSearchWithErrors() for the residual, parser-level problems the request can't catch.
Form-request + scope combo:
class SearchRequest extends FormRequest
{
public function rules(): array
{
return ['q' => ['nullable', 'string', 'max:100']];
}
}
// controller
$posts = Post::smartSearchWithErrors($request->validated('q'))->paginate(20);
Reusable search endpoint. If search is called from several places (index page, API, Livewire component), wrap the composition in a dedicated query object or a local scope so the error-retrieval step lives in one spot rather than being copy-pasted around the app.
Testing. Feature-test the endpoint, not the trait; assert both the result set and the error payload so the 2.3.0 behaviour is locked in:
public function test_search_reports_parse_errors()
{
$this->get('/posts?q=%%%')
->assertOk()
->assertViewHas('errors', fn ($errors) => $errors->isNotEmpty());
}
Version gate. smartSearchWithErrors() only exists from 2.3.0. If you support older installs (or a downstream package pins ^2.0), either bump the constraint to ^2.3 or feature-detect with method_exists(Model::class, 'scopeSmartSearchWithErrors').
Errors are probably stateful, and that bites in long-running processes. Traits that accumulate errors into a property will leak across queries in Octane, queue workers, and tests that reuse a model instance. Reset or re-instantiate between searches, and check whether the accessor reads from the model instance or a static/service container.
smartSearchWithErrors() still returns a builder. Do not expect a result set or a tuple. The errors arrive via a separate accessor after parsing has happened — which means reading them before the query executes may give you stale or empty data. Read them after you've built (and ideally run) the query, and confirm the ordering in the trait.
Errors don't survive pagination serialization. paginate() responses go through toArray()/toJson(); a trait-level error bag won't be in the payload. For JSON APIs, merge it explicitly:
return response()->json([
'data' => $posts->items(),
'meta' => [...],
'errors' => Post::searchErrors(),
]);
Watch the column whitelist. Search scopes that accept relation paths (author.name) or user-supplied column names are an injection vector if the column list is ever derived from request input. Keep $searchable hard-coded or strictly validated; never let a query parameter name the column.
LIKE %term% is a full table scan. Fine at small scale, painful past a few hundred thousand rows. Plan to move to a FULLTEXT index or an external driver; the scope should stay thin enough that you can swap the implementation behind it.
Scope-name collisions. smartSearch is generic enough to clash with an app-defined search or with another package's scope. If Laravel starts resolving the wrong one, rename at the call site or wrap it in your own named scope.
Debugging the generated query. When results look wrong, dump the SQL before blaming the package:
$q = Post::smartSearch($term);
dump($q->toSql(), $q->getBindings());
If the SQL looks right, the problem is in term parsing — which is exactly what the new error accessor is for. Reach for smartSearchWithErrors() as a diagnostic first, then keep it for the UI.
Extension points. The trait methods are protected/public scope methods and can be overridden in your model if you need custom term normalisation. Prefer overriding a single small method over forking the package; the SmartSearch class is the intended seam for behaviour changes, and 2.3.0's "cleanup and minor improvement" to it is a reminder that its internals are the most likely thing to shift between minor versions — pin the constraint and read the diff before upgrading.
Pin and test on minor bumps. This release is additive (new scope, cleanup elsewhere), so ^2.3 is safe. The SmartSearch cleanup is the one place a subtle behaviour change could hide, so run your search test suite against the upgrade rather than assuming additive means harmless.
How can I help you explore Laravel packages today?