thejano/laravel-filterable

Add filtration functionality to Laravel Models

1.4.0 2024-03-20 13:13 UTC

README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

This package adds filtration functionality to Laravel Models. It would be based on Filterable and Query Filter classes. The package will provide commands to generate Filterable and Query Filter classes. By default, it will add some default filtration out of the box to you models like ordering, get data between two dates and more.

Imagine you have a url containing the following parameters:

/posts?slug=the-new-web&published=true&category=web-development&tags[]=web&tags[]=laravel&tags[]=flutter

Laravel request all method request()->all() will return something like this:

[
    "slug"        => "the-new-web",
    "published"   => "true",
    "category"    => "web-development",
    "tags"        => [ "web", "laravel", "flutter"],
]

Normally, you should do the logic one by one to perform the filtration

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $query = Post::query();

        if ($request->has('title'))
        {
            $query->where('title', 'LIKE', '%' . $request->input('title') . '%');
        }
       
       if ($request->has('published'))
        {
            $query->where('published', (bool) $request->input('published'));
        }

        if ($request->has('category')){
            $query->whereHas('category', function ($query) use ($request)
            {
                return $query->where('category_slug', $request->input('category'));
            });
        }
        
        if ($request->has('tags')){
            $query->whereHas('tag', function ($query) use ($request)
            {
                return $query->where('tag_slug', $request->input('tags'));
            });
        }
        $posts = $query->get();
        return view('posts',compact('posts'));
    }

}

For simple queries, it would be fine, but when you have a bunch of filters, you can not control them and none of them are reusable.

With this package you need to only pass filterable() scope method to your model before returning the records, check below example:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Models\Post;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::filterable()->get();
        return view('posts',compact('posts'));
    }

}

Requirement

The package requires:

  • PHP 8.1 or higher
  • Laravel 10.x or higher

Installation

You can install the package via composer:

composer require thejano/laravel-filterable

You can publish the config file with:

php artisan vendor:publish --tag="filterable-config"

Usage

To add the magic you should add only hasFilterableTrait to your model.

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use TheJano\LaravelFilterable\Traits\HasFilterableTrait;

class Post extends Model
{
    use HasFactory;
    use HasFilterableTrait;
}

Then you need to create a FilterableClass and some Query Filters to define your rules.


To remove the pain of creating the classes, already I added some commands.

  • For creating a Filterable class, you need to run this command:
php artisan make:filterable PostsFilterable

It would generate a class under \App\Filters\Filterable\PostsFilterable and it contains:

<?php

namespace App\Filters\Filterable;

use TheJano\LaravelFilterable\Abstracts\FilterableAbstract;
use TheJano\LaravelFilterable\Interfaces\FilterableInterface;

class PostsFilterable extends FilterableAbstract implements FilterableInterface
{
    /**
     * It contains list of Query Filters
     *
     * @var Array
     */
    protected array $filters = [];
}

  • And now let's create a Query Filter class:
php artisan make:query-filter PublishedQueryFilter

It would generate a class under \App\Filters\QueryFilter\PublishedQueryFilter and it contains:

<?php

namespace App\Filters\QueryFilter;

use Illuminate\Database\Eloquent\Builder;
use TheJano\LaravelFilterable\Abstracts\QueryFilterAbstract;
use TheJano\LaravelFilterable\Interfaces\QueryFilterInterface;

class PublishedQueryFilter extends QueryFilterAbstract implements QueryFilterInterface
{
    /**
     * Can be used to map the values.
     * It can be returned through resolveValue method
     *
     * @var Array
    */
    protected array $mapValues = [];

    /**
     * Handle The Query Filter
     *
     *
     * @param Builder $builder Query Builder
     * @param string $value
     * @return Builder
    **/
    public function handle(Builder $builder, $value): Builder
    {
        return $builder;
    }
}

You will do the logic for each column inside each Query Filter separately. Let's implement the logic here

public function handle(Builder $builder, $value): Builder
{
    return $builder->where('published', $value);
}

The returned value is a string, and it does not return any data. So we should map the value.

There is a property $mapValues inside the class.

protected array $mapValues = [
    'true' => true,
    'false' => false,
];

And finally, we should resolve the mapped value through resolveValue() method.

protected array $mapValues = [
    'true' => true,
    'false' => false,
];

public function handle(Builder $builder, $value): Builder
{
    $value = $this->resolveValue($value);
    
    // return Builder if the value is null     
    if (is_null($value)) {
        return $builder;
    }

    return $builder->where('published', $value);
}

To make the Query Filter live, we should append it to the $columns property of PostsFilterable class.

public array $filters = [
    'published' => 'App\\Filters\\QueryFilter\\PublishedQueryFilter',
];

  • When we create a Query Filter directly you can pass the Filterable class as a parameter to auto-insert into$filters property.
php artisan make:query-filter PublishedQueryFilter --filterable=PostsFilterable

Now your PostsFilterable class should contain something like this:

<?php

namespace App\Filters\Filterable;

use TheJano\LaravelFilterable\Abstracts\FilterableAbstract;
use TheJano\LaravelFilterable\Interfaces\FilterableInterface;

class PostsFilterable extends FilterableAbstract implements FilterableInterface
{
    /**
     * It contains list of Query Filters
     *
     * @var Array
     */
    public array $filters = [
        'published' => 'App\\Filters\\QueryFilter\\PublishedQueryFilter',
    ];
}

The final step is enabling PostsFilterable to your model.

There are 3 ways to enable it:

  1. Do nothing :) It would enable all default Query Filters to your model.

  2. Passing Filterable class to filterableClass() method of the model.

<?php

namespace App\Models;

use App\Filters\Filterable\PostsFilterable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use TheJano\LaravelFilterable\Traits\HasFilterableTrait;

class Post extends Model
{
    use HasFactory;
    use HasFilterableTrait;

    /**
     * Enable the filterable class to the model
     *
     * @return void
     */
    public function filterableClass()
    {
        return PostsFilterable::class;
    }
}

  1. Passing as a parameter to fliterable() scope of your model. The scope accepts 3 parameters
public function scopeFilterable(Builder $builder, $request = null, $filterableClass = null, $filters = []): Builder

If you pass Filterable class as 1st parameter, under the hood the package will handle it for you and ignore the $request variable and uses the request() helper function. Let's check the code below.

<?php

namespace App\Http\Controllers;

use App\Filters\Filterable\PostFilterable;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::filterable(PostFilterable::class)->get();
        return view('posts', compact('posts'));
    }
}

Also, you can pass some additional Query Filters through $filters parameter, for example:

<?php

namespace App\Http\Controllers;

use App\Filters\Filterable\PostFilterable;
use App\Filters\QueryFilter\TitleQueryFilter;
use App\Models\Post;
use Illuminate\Http\Request;

class PostController extends Controller
{
    public function index(Request $request)
    {
        $posts = Post::filterable(PostFilterable::class,[
            'title' => TitleQueryFilter::class
        ])->get();
        return view('posts', compact('posts'));
    }
}

Instead only Request you can pass array of parameters to filter too.

Default Query Filters

Last but not least, by default the package deliveries some Query Filters with every Filterable class. The configuration file contains the available Query Filters, which are:


  1. date query filter, it returns records between 2 dates (from and to). By default, it uses created_at field.
/posts?date[from]=2022-06-01&date[to]=2022-07-01

Or you can pass a custom field. The delimiter is BY

/posts?date[fromBYupdated_at]=2022-06-01&date[toBYupdated_at]=2022-07-01

  1. order query filter, it orders the records as ASC or DESC. By default, it uses created_at field.
/posts?order=asc

Or you can pass a custom field.

/posts?order[id]=asc

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.