A library to handle filtering and sorting for your APIs
Filters like /pets?name:in=Snoopy,Hobbes
will be applied to your query builder instance and only results matching that criteria will return
First, use composer to require the package as below:
composer require ans-group/laravel-sieve
In Laravel the service provider will be automatically discovered.
If you are using Lumen you will need to register the service provider in the "Register Service Providers" section of bootstrap/app.php
:
$app->register(UKFast\Sieve\SieveServiceProvider::class);
Sieve uses an expressive API to configure these filters:
<?php
class Pet extends Model implements Searchable
{
public function sieve(Sieve $sieve)
{
$sieve->configure(fn ($filter) => [
'name' => $filter->string(),
'breed' => $filter->enum(['Beagle', 'Tiger']),
]);
}
}
<?php
class PetController extends Controller
{
public function index()
{
return Pets::query()->search()->paginate();
}
}
Filters are done in the query parameters with the format property:operator=term
, for example
name:eq=Bob
- WHERE name = 'Bob'age:lt=20
- WHERE age < 20employed:eq=true
- WHERE employed = 1
By default, if no operator is specified, it will default to eq
so name=Bob
will expand to name:eq=Bob
The string filter also supports like and not like operators. To use these you should use *
as the wildcard
rather than %
as you would in a typical database query.
For instance first_name:lk=Sam*
would match Sam
, Samuel
and Samantha
.
You can set the fields that can be sortable when setting up your model.
You can pass an array of fields as the second parameter of configure.
It does not have to match the filterable, allowing total control.
<?php
class Pet extends Model implements Searchable
{
public function sieve(Sieve $sieve)
{
$sieve->configure(fn ($filter) => [
'name' => $filter->string(),
'breed' => $filter->enum(['Beagle', 'Tiger']),
],
['name']
);
}
}
Sieve will also allow consumers of your API to specify sort order. You can do this by sort=property:direction
sort=age:asc
sort=id:desc
If you do not specify a direction (for instance sort=age
) then the sorting will be carried out in ascending order.
You can specify more than one column to sort by as a comma separated list. These will be applied in the order given.
For instance sort=age:asc,name:asc
will return people in age order, then alphabetically.
By default, MySQL will sort null
values first for ascending sorts and last for descending sorts. Depending on the context
of the column this may not be the desired functionality. You can change this using the following URL queries:
sort=priority:asc_nulls_last
sort=priority:desc_nulls_first
You can set a default sort using the setDefaultSort
on theSieve
class. The default sort uses the database column
name rather than the mapped property name, meaning you can use the default sort property without having a map set up.
$sieve->setDefaultSort('name', 'asc')
The basic filter. Ideal for textual data, implements eq
, neq
, in
, nin
, lk
and nlk
<?php
$filter->string()
Ideal for numerical data, on top of the basic operators, it also provides lt
(less than) and gt
(greater than)
<?php
$filter->numeric()
Same as the string filter but with extra validation. Will throw an exception if the user gives an invalid value
<?php
$filter->enum(['HR', 'RnD'])
Only provides eq
and neq
. Also takes two arguments to specify what your true and false values are in the DB
<?php
$filter->boolean() // defaults to 1 and 0
$filter->boolean('Yes', 'No') // search for Yes and No in the database
Provides the same operations as numeric
<?php
$filter->date(),
You can get type hinting for these filters by specifying FilterBuilder
when using the configure method
<?php
$sieve->configure(fn (FilterBuilder $filter) [
'name' => $filter->string(),
]);
You can filter on relationships using the for
method on the filter builder
<?php
$sieve->configure(fn ($filter) => [
'owner_name' => $filter->for('owner.name')->string()
])
You can also use the for
method if your API properties don't match column names in the database, for example
<?php
$sieve->configure(fn ($filter) => [
'date_created' => $filter->for('created_at')->date()
])
You can easily wrap all the available filters to decorate their behaviour. A good example of this is if your API has some kind of computed property
A very simple example might be you have a price
property that's stored in pence in the DB, but displayed as pounds in the API (not necessarily good practice, but works for the example)
We can still filter on this by setting up a wrapper that will modify the consumers query before passing it to the filter:
<?php
class PenceFilter implements WrapsFilter
{
protected $filter;
public function setFilter($filter)
{
$this->filter = $filter;
}
public function modifyQuery($query, SearchTerm $search)
{
$pence = $search->term() * 100;
// will do a better API for making new search terms later
$newTerm = new SearchTerm($search->property(), $search->operator(), $search->column(), $pence);
$this->filter->modifyQuery($query, $search);
}
public function operators()
{
return $this->filter->operators();
}
}
This can be used by doing the following:
$sieve->configure(fn ($filter) => [
'price' => $filter->wrap(new PenceFilter)->numeric(),
])
Now when the user searches for price:eq=1.50
the database query will instead be WHERE price = 150
It's worth noting that wrap can be called multiple times and the builder will repeatedly re-wrap
We welcome contributions to this package that will be beneficial to the community.
You can reach out to our open-source team via open-source@ukfast.co.uk who will get back to you as soon as possible.
Please refer to our CONTRIBUTING file for more information.
If you think you have identified a security vulnerability, please contact our team via security@ukfast.co.uk who will get back to you as soon as possible, rather than using the issue tracker.
This project is licenced under the MIT Licence (MIT). Please see the Licence file for more information.