lucasbedout/detective
Detective is a Laravel package for inspecting and diagnosing application behavior, giving developers a clearer view of what happens under the hood so debugging and development investigations are faster and easier.
The package is small and thinly documented, so treat the source as the spec. First step is to read src/ and the README in the repo before writing any code — method names and config keys are not stable knowledge in your head, and there is no current documentation site.
Install
composer require lucasbedout/detective
Register the provider (pre-Laravel 5.5 auto-discovery)
// config/app.php
'providers' => [
// ...
LucasBedout\Detective\DetectiveServiceProvider::class,
],
If the package ships a config file, publish it and read every key before touching it:
php artisan vendor:publish --provider="LucasBedout\Detective\DetectiveServiceProvider"
Wire a model
The core of the package is a trait applied to an Eloquent model plus a declaration of which columns are filterable. Check the trait name in src/ — it is the single most important thing to confirm, because everything downstream depends on it.
use LucasBedout\Detective\...; // confirm the trait namespace
class Post extends Model
{
use /* the detective trait */;
protected $filterable = [
'title',
'status',
'created_at',
];
}
First use case
An index endpoint that accepts query string filters instead of hand-rolled if ($request->has(...)) chains:
public function index(Request $request)
{
return Post::filter($request->only(['title', 'status', 'created_at']))
->paginate(25);
}
That is the whole value proposition: one scope call replaces a dozen conditional where clauses. Get that working on one resource before adopting it anywhere else.
Whitelist at the model, not the controller. The safest pattern with any request-driven filter is to keep the allowlist in the model's $filterable property and never trust the query string. Pass $request->only([...]) from the controller so unknown keys never reach the scope at all. Two layers of defence is cheap here.
Dedicated filter request class. Rather than validating inline, move the contract into a form request so the filter surface is documented and testable:
class PostFilterRequest extends FormRequest
{
public function rules()
{
return [
'title' => 'sometimes|string|max:255',
'status' => 'sometimes|in:draft,published',
'created_at' => 'sometimes|date',
];
}
}
Then Post::filter($request->validated()). This is the pattern I would standardize on across a codebase — it keeps the controller to three lines and makes the available filters discoverable.
Compose with existing scopes, don't replace them. Detective should slot in alongside your domain scopes, not swallow them:
Post::query()
->published()
->filter($request->validated())
->with('author')
->latest()
->paginate(25);
Centralize the index pattern. If you have ten resources, write one abstract base controller or a small Filterable concern that takes a model class and a request, so filtering behaviour is identical everywhere. Divergent per-controller filtering is how these packages rot.
Sorting and pagination. If the package supports sort parameters, keep sort keys on the same allowlist as filters — an unvalidated orderBy from the query string is an enumeration/DoS vector even when it isn't injectable. Always pair with paginate(), never get(), on user-facing endpoints.
Testing. Because the package is unmaintained, wrap it in feature tests rather than trusting it. One test per filterable column asserting the generated SQL or result set, plus a test asserting a disallowed column is ignored:
public function test_unknown_filter_is_ignored()
{
$this->get('/posts?password=secret')
->assertOk();
// assert the query did not filter on password
}
Those tests are your upgrade insurance if you ever migrate off the package.
Migration path in the same PR. Since the package is a thin layer, isolate it behind your own scope (scopeSearch() on a base model) so replacing it later with spatie/laravel-query-builder or plain query-builder code is a one-file change rather than a grep across the app.
It is effectively abandoned. Last release was October 2016, 13 stars, no maintenance. It predates Laravel 5.5 auto-discovery, Arr::only semantics you may rely on, and modern PHP type handling. On Laravel 8+ expect provider registration issues, potential deprecation warnings, and no fixes. Do not add it to a new project — vendor the relevant logic instead, which for a filtering trait is usually under 150 lines.
Verify framework compatibility before installing. Pin the package against your Laravel version in composer.json and run the full test suite immediately after install. An abandoned package resolving against a modern Laravel can silently break other dependencies during composer update.
Request mass-assignment is the classic footgun. If the package's examples show Model::filter($request->all()), do not copy that. $request->all() includes _token, page, and anything else a client sends. Always ->only() or ->validated().
Ambiguous columns on joins. Filtering on created_at when the query joins authors (which also has created_at) produces an ambiguous-column SQL error. Qualify columns in your $filterable list (posts.created_at) if the package passes them through verbatim — check the source for whether it table-qualifies.
LIKE filters and wildcards. String filters typically map to LIKE %value%. That means a user typing % matches everything, and leading wildcards prevent index use on large tables. For high-traffic endpoints, prefer prefix matching or full-text search rather than relying on the package's default.
Operator parsing is the injection surface. If the package supports operators (>, <, !=) embedded in values or keys, confirm they are validated against a fixed list. If not, do not expose operator syntax to untrusted input at all.
Debug with the query log, not guesswork. DB::listen(fn ($q) => logger($q->sql, $q->bindings)) or ->toSql() on the built query tells you exactly what the scope produced. With minimal docs, reading the generated SQL is faster than reading the source.
Extension points. The realistic customisation hooks are: overriding the filter-resolution method in your model's trait usage (insteadof/alias), adding your own scopes before/after the filter call, or subclassing the service provider. Beyond that, you are patching vendor code — which is the signal to fork or replace.
Have an exit plan documented. Note in your repo's README or an ADR that filtering depends on an unmaintained package, what the replacement is, and which tests cover it. That single paragraph saves the next developer a day of archaeology.
How can I help you explore Laravel packages today?