agog/osmose
Osmose is a Laravel package for elegantly filtering Eloquent queries via dedicated filter classes. Generate filters with an artisan command, define rules in a residue() array, and apply them with sieve() using built-in direct, callback, and relationship drivers.
Installation:
composer require agog/osmose
Generate a Filter:
php artisan osmose:make-filter UserFilter
This creates app/Http/Filters/UserFilter.php with a scaffolded residue() method.
First Use Case: Inject the filter into a controller method and apply it to an Eloquent query:
public function index(UserFilter $filter)
{
$users = $filter->sieve(User::class)->get();
return response()->json($users);
}
Define Basic Rules:
Edit residue() in UserFilter.php to include direct column filters:
public function residue(): array
{
return [
'active' => 'column:is_active',
'role' => 'relationship:roles,name',
];
}
Test the Filter: Call the endpoint with query parameters:
GET /users?active=1&role=admin
public function show(UserFilter $filter, int $id)
{
$user = $filter->sieve(User::query()->where('id', $id))->first();
}
osmose() for concise syntax (requires publishing config):
$users = osmose(UserFilter::class)->get();
column:status,active).
'status' => 'column:status',
relationship:posts,title).
'post_title' => 'relationship:posts,title',
where clauses).
'search' => function ($query, $value) {
return $query->where('name', 'like', "%{$value}%");
},
public function bound(): array
{
return [
'column:is_admin,1',
function ($query) {
return $query->where('deleted_at', null);
},
];
}
php artisan vendor:publish --provider="Agog\Osmose\Providers\OsmoseServiceProvider") and define ranges in config/osmose.php:
'ranges' => [
'week' => '7 days',
'month' => '1 month',
],
public function column(): string { return 'created_at'; }
public function range(): string { return 'week'; }
public function limits(): array { return ['from' => 'from', 'to' => 'to']; }
GET /users?from=2023-01-01&to=2023-01-31&range=month
'dynamic_field' => function ($query, $value) {
if ($value === 'all') {
return $query->whereNull('deleted_at');
}
return $query->where('status', $value);
},
ids=1,2,3):
'ids' => 'column:id',
public function index(Request $request, UserFilter $filter)
{
$validated = $request->validate([
'active' => 'sometimes|boolean',
'role' => 'sometimes|string',
]);
$users = $filter->sieve(User::class)->get();
}
$users = $filter->sieve(User::class)->paginate(10);
Namespace Organization:
App/Http/Filters/Admin/UserFilter, App/Http/Filters/API/ProductFilter).trait ActiveFilterTrait {
public function residue(): array {
return ['active' => 'column:is_active'];
}
}
Testing:
$filter = Mockery::mock(UserFilter::class);
$filter->shouldReceive('sieve')->andReturn(User::query()->where('active', 1));
Performance:
with() in the sieve method to avoid N+1 queries:
$filter->sieve(User::query()->with('roles'))->get();
Documentation:
/**
* Filters users by role and active status.
*
* @queryParam role string Filter by role name (e.g., ?role=admin).
* @queryParam active boolean Filter by active status (e.g., ?active=1).
*/
class UserFilter extends OsmoseFilter { ... }
Error Handling:
try {
return $filter->sieve(User::class)->get();
} catch (\Exception $e) {
return response()->json(['error' => 'Invalid filter'], 400);
}
Case Sensitivity:
relationship:roles,name vs. relationship:Roles,name). Ensure consistency in model definitions.Bound Rules Execution:
bound()) always execute, even if no query params are provided. Use sparingly for mandatory filters (e.g., soft-deletes, admin checks).Date Range Quirks:
range method requires the osmose.php config to be published. Forgetting this step will silently ignore range filters.$request->validate(['from' => 'date']);
Callback Filter Overhead:
// Skips the callback if 'search' is not in the request.
'search' => function ($query, $value) { ... },
Global Function Limitations:
osmose() function relies on naming conventions (UserFilter → User). Override the model namespace in config/osmose.php if models are in non-standard locations:
'model_namespace' => 'App\\Models',
Direct Filter Delimiters:
ids=1,2,3) require a comma delimiter. Missing this will cause the filter to fail silently.PHP 8 Type Safety:
public function residue(): array { ... } // Must return array.
public function column(): string { ... } // Must return string.
Log Filter Rules: Add debug logs to inspect applied rules:
public function residue(): array {
\Log::debug('Filter rules:', ['rules' => [
'active' => 'column:is_active',
'role' => 'relationship:roles,name',
]]);
return [...];
}
Query Builder Inspection: Use Laravel’s query logging to verify the final SQL:
\DB::enableQueryLog();
$users = $filter->sieve(User::class)->get();
\Log::debug('Final query:', ['query' => \DB::getQueryLog()]);
Test with Hardcoded Values:
Temporarily hardcode values in residue() to isolate issues:
public function residue(): array {
return [
'active' => 'column:is_active', // Hardcode to test
'
How can I help you explore Laravel packages today?