albetnov / laravel-filterable
Query String based filter for Laravel
Requires
- php: ^8.1
- illuminate/contracts: ^10.44|11.*
- laravel/framework: ^10.44|11.*
- nesbot/carbon: ^2.67|3.*
- orchestra/testbench: 8.*|9.*
- spatie/laravel-package-tools: ^1.16.0
Requires (Dev)
- laravel/pint: ^1.0
- mockery/mockery: ^1.6
- nunomaduro/collision: ^7.10|8.*
- nunomaduro/larastan: ^2.9
- pestphp/pest: ^2.34.0
- pestphp/pest-plugin-arch: ^2.3
- pestphp/pest-plugin-laravel: ^2.3
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1
- phpstan/phpstan-phpunit: ^1.3
This package is auto-updated.
Last update: 2024-11-08 02:23:16 UTC
README
A Laravel Model Filterable to automatically filter a model based on given query string
Installation
You can install the package via composer:
composer require albetnov/laravel-filterable
Usage
Simply add Filterable
trait in your model and define either $filterableColumns
or filterableColumns()
(if you need extra logic) to define a filterable columns:
<?php namespace App\Models; use Albet\LaravelFilterable\Enums\FilterableType; use Albet\LaravelFilterable\Traits\Filterable; use Illuminate\Database\Eloquent\Model; class Flight extends Model { use Filterable; protected arrray $filterableColumns = [ 'ticket_no' => FilterableType::NUMBER, 'customer_name' => FilterableType::TEXT, 'schedule' => FilterableType::DATE ]; protected function filterableColumns(): array { return [ 'customer_address' => FilterableType::custom(), ]; } }
Getting Filterable Columns
If both are defined, Filterable will priority the method over property. As defined in: Filterable.php
If none exist though, Filterable will throws PropertyNotExist
exception.
Filterable Type
There are Five FilterableType options available:
-
Number
This type will cast the given payload to either float or int, depending on whether the string contains a . prefix (indicating a float) or not (indicating an int). -
Text
A type designed for handling text-based filters. -
Date
A type intended for handling date-based filters. This will cast the payload to the Carbon format and adjust the query accordingly. -
Boolean
A type intended for handling boolean-based filters. This type will cast the payload to boolean depending on0
and1
.
Modifiers
Each of the FilterableType
supports a modifier to alter the behaviour of filtering from the assigned field.
There are 2 modifiers you can use for now.
limit
Allows you to limit the available operator, leaving the rest of the operators become invalid and throwsOperatorNotExist
exception. The function have one argument receiving an array ofOperators
. Usage Example:
use Albet\LaravelFilterable\Enums\FilterableType; use Albet\LaravelFilterable\Enums\Operators; protected function filterableColumns(): array { return [ 'customer_name' => FilterableType::TEXT->limit([Operators::CONTAINS, Operators::NOT_CONTAINS, Operators::STARTS_WITH, Operators::ENDS_WITH]) ]; }
related
Allows you to replace the query to usewhereHas
so that the filter apply to relation level. The function receives 2 arguments, first is the relationship name, and second is the extra query condition which are optional. Usage Example:
use Albet\LaravelFilterable\Enums\FilterableType; use Illuminate\Database\Eloquent\Relations\HasOne; protected function filterableColumns(): array { return [ 'flight_license' => FilterableType::NUMBER->related('flight', fn($query) => $query->where('status', 'A')) ]; } public function flight(): HasOne { $this->hasOne(Flight::class); }
The modifiers can be chained together:
FilterableType::DATE->related()->limit()
to combine conditions
Custom Type
Custom Type does not support modifier.
As mentioned before there are 5 types exist for Filterable, the last one is custom
which are treated differently.
Custom Type is part of static method of FilterableType
and therefore requires you to define it in filterableColumns()
method.
use Albet\LaravelFilterable\Enums\FilterableType; FilterableType::custom();
The custom method accept one argument, $allowedOperators
that are an array of Operators
. This argument used to define
the whitelist of allowed operators for your custom filter.
The custom type requires a handler, both of the handler and the field have to be defined under this convention:
use Albet\LaravelFilterable\Enums\FilterableType; use Albet\LaravelFilterable\Enums\Operators; use Albet\LaravelFilterable\Operator; use Albet\LaravelFilterable\Traits\Filterable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; class Flight extends Model { use Filterable; public function filterableColumns(): array{ return [ 'customer_address' => FilterableType::custom([Operators::CONTAINS, Operators::NOT_CONTAINS]) ]; } public function filterCustomAddress(Builder $builder, string $operator, string $value): void { dump($operator); // 'contains' or 'not_contains' (raw string operator) dump($value); // raw string value $builder->whereHas('customer', fn($query) => $query->where('name', 'LIKE', "%$value%")); } }
Notice that the columns are defined in snake case while the method is camel case. Your function should also have three
arguments defined. First is the Builder
, a raw operator
, and finally a raw value
. The raw
term means that these
value are not formatted and they are passed quickly from the query string. However, they are validated.
Using in model
Just call filter scope and you're set:
<?php use App\Models\Flight; // In this example I choose to merge the request, alternatively you can hit endpoint like this: // http://localhost:8000/all-flights?filters[0][field]=customer_name&filters[0][operator]=eq&filters[0][value]=asep request()->merge([ 'filters' => [ [ 'operator' => 'eq', 'field' => 'customer_name', 'value' => 'asep' ] ] ]); dd(Flight::filter()->get()); // Flight[{customer_name: "asep", ticket_no: 20393, schedule: "2023-08-20"}]
Request Schema
Here is the expected request payload schema that can be read by Laravel Filterable:
{ "filters": [ { "operator": "eq", "field": "customer_name", "value": "asep" } ] }
All the filters must be placed within filters
key and the values should be an array of object
containing:
fields
(String)
Determine which field should be filteredoperator
(String)
What kind of the operator to be used in the filter contextvalue
(String)
The expected value
The above schema when mapped to query string will be like this:
?filters[0][field]=customer_name&filters[0][operator]=eq&filters[0][value]=asep
Supported Operators
eq
: Checks if the value is equal to the specified input.neq
: Checks if the value is not equal to the specified input.contains
: Checks if the value contains the specified input.starts_with
: Checks if the value starts with the specified input.ends_with
: Checks if the value ends with the specified input.not_contains
: Checks if the value does not contain the specified input.in
(array): Checks if the value is one of the specified inputs.not_in
(array): Checks if the value is not any of the specified inputs.have_all
(array): Checks if the value has all the specified inputs.gt
: Checks if the value is greater than the specified input.lt
: Checks if the value is less than the specified input.gte
: Checks if the value is greater than or equal to the specified input.lte
: Checks if the value is less than or equal to the specified input.
Limitations
The value is limited exclusively to the string
type, and each casting is performed through types defined in
filterableColumns
. Ambiguous casting may occur for arrays
with items containing ,
as the value delimiter.
Please avoid using ,
in your values, as they are used as the internal array delimiter. Another case could involve
numbers
with more than one .
delimiter.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.