Simple filter and paginate index data. Main idea - KISS.
Example query:
?search_description=some%20text&sort_id=desc&where_publisher_id=1,23&where_has_groups__id=30,40&limit=10&offset=20
In code this query reflects:
- search_description=some%20text -
$builder->where('description', 'like', '%some%text%')
- sort_id=desc -
$builder->sortBy('id', 'desc')
- where_publisher_id=1,23 -
$builder->whereIn('publisher_id', [1,23])
- where_has_groups__id=30,40 -
$builder->whereHas('groups', fn($builder) => $builder->whereIn('id', [30,40]))
- limit=10 -
$builder->limit(10)
- offset=20 -
$builder->offset(20)
Requires laravel >= 9 and php ^8.1
composer require freebuu/laravel-filterable
Add HasRequestFilter trait to Model - that's all.
class PostIndexController
{
public function __invoke(Request $request)
{
$data = Post::requestFilter()->setResource(PostResource::class);
return response()->json($data);
}
}
Example result for query /api/posts/?limit=25&offset=10
{
"meta": { // object with pagination data
"limit": 25,
"offset": 10,
"total": 2
},
"data": [ //array with model data, wrapped in `PostResource` (of course you can not use resource and simply output collection or pass collection to view)
{
"id": "1",
"title": "post title"
},
{
"id": "2",
"title": "another post title"
}
]
}
For filtration, you need to create filter class for each model. Filter class must extend AbstractFilter. Best place for these classes is App\Http\Filters
.
In method getFilterableFields()
you specify which fields can be filtered in each filter case.
HINT - always add default
state because filter cases may be supplemented.
class PostFilter extends AbstractFilter
{
protected function getFilterableFields(FilterCaseEnum $case): array
{
return match ($case) {
FilterCaseEnum::WHERE => ['publisher_id'],
FilterCaseEnum::SORT => ['id'],
FilterCaseEnum::WHERE_HAS => ['groups' => ['id']]
default => []
};
}
}
To set this filter for Model
- overwrite requestFilterClass()
method
class Post extends Model
{
use HasRequestFilter;
public function requestFilterClass(): string
{
return \App\Http\Filters\PostFilter::class;
}
}
Filterable query params contains four parts separated with _
. Let's see example with where_has_groups__id=30,40
- $case - where_has
- $field - groups
- $fieldValue - id (optional, mandatory only with where_has)
- $value - 30,40
In code, they are presented as FilterCaseEnum.php and they work like this
- FROM
- Accepts only int
$builder->where($field, '>=', $value)
- TO
- Accepts only int
$builder->where($field, '<=', $value)
-
- SORT - sorting
- Accepts only
asc
,desc
$builder->sortBy($field, $value)
- Accepts only
- SEARCH - search with
like
operator- Accept string with spaces. Add
%
at start, end and instead of all spaces $builder->where($field', 'like', $value)
- Accept string with spaces. Add
- START_WITH - all strings starts with passed value
- Accept string without spaces. Add
%
at end of value $builder->where($field', 'like', $value)
- Accept string without spaces. Add
- WHERE_HAS - filter by relation with array of values
- Accept comma separated array of values.
- For this filter
fieldValue
fields must be set (see in example inPostFilter
) $builder->whereHas($field, fn($builder) => $builder->whereIn($fieldValue, $value))
- WHERE - filter by array of values
- Accept comma separated array of values.
$builder->whereIn($fieldValue, $value)
- FILTER - uses for custom filters, see below
In filter class you can make custom filter by creating a method like filterCustom
- it must begin with filter
. Then yoy can use it in query like ?filter_custom=123
HINT - you can use fieldValue
here like ?filter_custom__alias=123
- it pass as third parameter in filter method.
class PostFilter extends AbstractFilter
{
public function filterCustom(Builder $builder, mixed $value, mixed $fieldValue): void
{
//you have request instance here
if($this->request->query('something')){
return;
}
//$value will be 123
//$fieldValue will be alias (or can be null)
$builder->where('some_field', $value)->where($fieldValue, '432')
}
}
For security reasons, the limit
field is set to a maximum value. If the request specifies a value greater, it will be reset to the default value.
- Default it set to
30
- You can override this value in Filter class - overwrite the
maxLimit
property. - Or you can override it system-wide in
AppServiceProvider
class AppServiceProvider extends ServiceProvider
{
public function register()
{
AbstractFilter::$defaultMaxLimit = 50;
}
}
To set up resource - just pass resource class like here Post::requestFilter()->response(PostResource::class)
.
Sometimes you need to set up query condition situational - e.g. filter only for auth user
Post::requestFilter()
->addQueryCallback(fn (Builder $builder) => $builder->where('author_id', auth()->id()))
->response(ResourceClass::class);