tucker-eric/eloquentfilter

An Eloquent way to filter Eloquent Models


Keywords
search, filter, query, model, laravel, eloquent, dynamic-filters, eloquent-filters, eloquent-models, eloquentfilter, filter-logic, laravel-filter, model-filters, relation-filter
License
MIT

Documentation

Eloquent Filter

Latest Stable Version Total Downloads Daily Downloads License StyleCI Build Status

An Eloquent way to filter Eloquent Models and their relationships

Introduction

Lets say we want to return a list of users filtered by multiple parameters. When we navigate to:

/users?name=er&last_name=&company_id=2&roles[]=1&roles[]=4&roles[]=7&industry=5

$request->all() will return:

[
    'name'       => 'er',
    'last_name'  => '',
    'company_id' => '2',
    'roles'      => ['1','4','7'],
    'industry'   => '5'
]

To filter by all those parameters we would need to do something like:

<?php namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\User;

class UserController extends Controller
{

    public function index(Request $request)
    {
        $query = User::where('company_id', $request->input('company_id'));

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

        if ($request->has('name'))
        {
            $query->where(function ($q) use ($request)
            {
                return $q->where('first_name', 'LIKE', $request->input('name') . '%')
                    ->orWhere('last_name', 'LIKE', '%' . $request->input('name') . '%');
            });
        }

        $query->whereHas('roles', function ($q) use ($request)
        {
            return $q->whereIn('id', $request->input('roles'));
        })
            ->whereHas('clients', function ($q) use ($request)
            {
                return $q->whereHas('industry_id', $request->input('industry'));
            });

        return $query->get();
    }

}

To filter that same input With Eloquent Filters:

<?php namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\User;

class UserController extends Controller
{

    public function index(Request $request)
    {
        return User::filter($request->all())->get();
    }

}

Configuration

Install Through Composer

composer require tucker-eric/eloquentfilter

There are a few ways to define the filter a model will use:

Default Settings

The default namespace for all filters is App\ModelFilters\ and each Model expects the filter classname to follow the {$ModelName}Filter naming convention regardless of the namespace the model is in. Here is an example of Models and their respective filters based on the default naming convention.

Model ModelFilter
App\User App\ModelFilters\UserFilter
App\FrontEnd\PrivatePost App\ModelFilters\PrivatePostFilter
App\FrontEnd\Public\GuestPost App\ModelFilters\GuestPostFilter

With Configuration File (Optional)

Registering the service provider will give you access to the php artisan model:filter {model} command as well as allow you to publish the configuration file. Registering the service provider is not required and only needed if you want to change the default namespace or use the artisan command

After installing the Eloquent Filter library, register the EloquentFilter\ServiceProvider::class in your config/app.php configuration file:

'providers' => [
    // Other service providers...

    EloquentFilter\ServiceProvider::class,
],

Copy the package config to your local config with the publish command:

php artisan vendor:publish --provider="EloquentFilter\ServiceProvider"

In the config/eloquentfilter.php config file. Set the namespace your model filters will reside in:

'namespace' => "App\\ModelFilters\\",

Define The Default Model Filter

Create a public method modelFilter() that returns $this->provideFilter(Your\Model\Filter::class); in your model.

<?php namespace App;

use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use Filterable;

    public function modelFilter()
    {
        return $this->provideFilter(App\ModelFilters\CustomFilters\CustomUserFilter::class);
    }

    //User Class
}

Dynamic Filters

You can define the filter dynamically by passing the filter to use as the second parameter of the filter() method. Defining a filter dynamically will take precedent over any other filters defined for the model.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;

use App\Http\Requests;
use App\User;
use App\ModelFilters\Admin\UserFilter as AdminFilter;
use App\ModelFilters\User\UserFilter as BasicUserFilter;
use Auth;

class UserController extends Controller
{
    public function index(Request $request)
    {
        $userFilter = Auth::user()->isAdmin() ? AdminFilter::class : BasicUserFilter::class;

        return User::filter($request->all(), $userFilter)->get();
    }
}

Generating The Filter

Only available if you have registered EloquentFilter\ServiceProvider::class in the providers array in your `config/app.php'

You can create a model filter with the following artisan command:

php artisan model:filter User

Where User is the Eloquent Model you are creating the filter for. This will create app/ModelFilters/UserFilter.php

The command also supports psr-4 namespacing for creating filters. You just need to make sure you escape the backslashes in the class name. For example:

php artisan model:filter AdminFilters\\User

This would create app/ModelFilters/AdminFilters/UserFilter.php

Usage

Defining The Filter Logic

Define the filter logic based on the camel cased input key passed to the filter() method.

  • Empty strings are ignored
  • setup() will be called regardless of input
  • _id is dropped from the end of the input to define the method so filtering user_id would use the user() method
  • Input without a corresponding filter method are ignored
  • The value of the key is injected into the method
  • All values are accessible through the $this->input() method or a single value by key $this->input($key)
  • All Eloquent Builder methods are accessible in this context in the model filter class.

To define methods for the following input:

[
    'company_id'   => 5,
    'name'         => 'Tuck',
    'mobile_phone' => '888555'
]

You would use the following methods:

class UserFilter extends ModelFilter
{
    // This will filter 'company_id' OR 'company'
    public function company($id)
    {
        return $this->where('company_id', $id);
    }

    public function name($name)
    {
        return $this->where(function($q) use ($name)
        {
            return $q->where('first_name', 'LIKE', "%$name%")
                ->orWhere('last_name', 'LIKE', "%$name%");
        });
    }

    public function mobilePhone($phone)
    {
        return $this->where('mobile_phone', 'LIKE', "$phone%");
    }

    public function setup()
    {
        $this->onlyShowDeletedForAdmins();
    }

    public function onlyShowDeletedForAdmins()
    {
        if(Auth::user()->isAdmin())
        {
            $this->withTrashed();
        }
    }
}

Note: In the above example if you do not want _id dropped from the end of the input you can set protected $drop_id = false on your filter class. Doing this would allow you to have a company() filter method as well as a companyId() filter method.

Note: In the example above all methods inside setup() will be called every time filter() is called on the model

Additional Filter Methods

The Filterable trait also comes with the below query builder helper methods:

EloquentFilter Method QueryBuilder Equivalent
$this->whereLike($column, $string) $query->where($column, 'LIKE', '%'.$string.'%')
$this->whereBeginsWith($column, $string) $query->where($column, 'LIKE', $string.'%')
$this->whereEndsWith($column, $string) $query->where($column, 'LIKE', '%'.$string)

Since these methods are part of the Filterable trait they are accessible from any model that implements the trait without the need to call in the Model's EloquentFilter.

Applying The Filter To A Model

Implement the EloquentFilter\Filterable trait on any Eloquent model:

<?php namespace App;

use EloquentFilter\Filterable;
use Illuminate\Database\Eloquent\Model;

class User extends Model
{
    use Filterable;

    //User Class
}

This gives you access to the filter() method that accepts an array of input:

class UserController extends Controller
{
    public function index(Request $request)
    {
        return User::filter($request->all())->get();
    }
}

Filtering By Relationships

There are two ways to filter by related models. Using the $relations array to define the input to be injected into the related Model's filter. If the related model doesn't have a model filter of it's own or you just want to define how to filter that relationship locally instead of adding the logic to that Model's filter then use the related() method to filter by a related model that doesn't have a ModelFilter. You can even combine the 2 and define which input fields in the $relations array you want to use that Model's filter for as well as use the related() method to define local methods on that same relation. Both methods nest the filter constraints into the same whereHas() query on that relation.

For both examples we will use the following models:

A App\User that hasMany App\Client::class:

class User extends Model
{
    use Filterable;

    public function clients()
    {
        return $this->hasMany(Client::class);
    }
}

And each App\Client belongs to App\Industry::class:

class Client extends Model
{
    use Filterable;

    public function industry()
    {
        return $this->belongsTo(Industry::class);
    }

    public function scopeHasRevenue($query)
    {
        return $query->where('total_revenue', '>', 0);
    }
}

We want to query our users and filter them by the industry and volume potential of their clients that have done revenue in the past.

Input used to filter:

$input = [
    'industry'         => '5',
    'potential_volume' => '10000'
];

Setup

Both methods will invoke a setup query on the relationship that will be called EVERY time this relationship is queried. The setup methods signature is {$related}Setup() and is injected with an instance of that relations query builder. For this example let's say when querying users by their clients I only ever want to show agents that have clients with revenue. Without choosing wich method to put it in (because sometimes we may not have all the input and miss the scope all together if we choose the wrong one) and to avoid query duplication by placing that constraint on ALL methods for that relation we call the related setup method in the UserFilter like:

class UserFilter extends ModelFilter
{
    public function clientsSetup($query)
    {
        return $query->hasRevenue();
    }
}

This prepend all queries with the hasRevenue() whenever the UserFilter runs any constriants on the clients() relationship. If there are no queries to the clients() relationship then this method will not be invoked.

You can learn more about scopes here

Ways To Filter Related Models

Filter Related Models With The related() Method:

The related() method is a little easier to setup and is great if you aren't going to be using the related Model's filter to ever filter that Model explicitly. The related() method takes the same parameters as the Eloquent\Builder's where() method except for the first parameter being the relationship name.

Example:

UserFilter with an industry() method that uses the ModelFilter's related() method

class UserFilter extends ModelFilter
{
    public function industry($id)
    {
        return $this->related('clients', 'industry_id', '=', $id);

        // This would also be shorthand for the same query
        // return $this->related('clients', 'industry_id', $id);
    }

    public function potentialVolume($volume)
    {
        return $this->related('clients', 'potential_volume', '>=', $volume);
    }
}

Or you can even pass a closure as the second argument which will inject an instance of the related model's query builder like:

    $this->related('clients', function($query) use ($id) {
        return $query->where('industry_id', $id);
    });

Filter Related Models Using The $relations Array:

Add the relation in the $relations array with the name of the relation as referred to on the model as the key and an array of input keys that was passed to the filter() method.

The related model MUST have a ModelFilter associated with it. We instantiate the related model's filter and use the input values from the $relations array to call the associated methods.

This is helpful when querying multiple columns on a relation's table while avoiding multipe whereHas() calls for the same relationship. For a single column using a $this->whereHas() method in the model filter works just fine. In fact, under ther hood the model filter applies all constraints in the whereHas() method.

Example:

UserFilter with the relation defined so it's able to be queried.

class UserFilter extends ModelFilter
{
    public $relations = [
        'clients' => ['industry', 'potential_volume'],
    ];
}

ClientFilter with the industry method that's used to filter:

Note: The $relations array should identify the relation and the input key to filter by that relation. Just as the ModelFilter works, this will access the camelCased method on that relation's filter. If the above example was using the key industry_type for the input the relations array would be $relations = ['clients' => ['industry_type']] and the ClientFilter would have the method industryType().

class ClientFilter extends ModelFilter
{
    public $relations = [];

    public function industry($id)
    {
        return $this->where('industry_id', $id);
    }

    public function potentialVolume($volume)
    {
        return $this->where('potential_volume', '>=', $volume);
    }
}

Filter Related Models With Both Methods

You can even use both together and it will produce the same result and only query the related model once. An example would be:

If the following array is passed to the filter() method:

[
    'name'              => 'er',
    'last_name'         => ''
    'company_id'        => 2,
    'roles'             => [1,4,7],
    'industry'          => 5,
    'potential_volume' => '10000'
]

In app/ModelFilters/UserFilter.php:

<?php namespace App\ModelFilters;

use EloquentFilter\ModelFilter;

class UserFilter extends ModelFilter
{
    public $relations = [
        'clients' => ['industry'],
    ];

    public function clientsSetup($query)
    {
        return $query->hasRevenue();
    }

    public function name($name)
    {
        return $this->where(function($q)
        {
            return $q->where('first_name', 'LIKE', $name . '%')->orWhere('last_name', 'LIKE', '%' . $name.'%');
        });
    }

    public function potentialVolume($volume)
    {
        return $this->related('clients', 'potential_volume', '>=', $volume);
    }

    public function lastName($lastName)
    {
        return $this->where('last_name', 'LIKE', '%' . $lastName);
    }

    public function company($id)
    {
        return $this->where('company_id',$id);
    }

    public function roles($ids)
    {
        return $this->whereHas('roles', function($query) use ($ids)
        {
            return $query->whereIn('id', $ids);
        });
    }
}
Adding Relation Values To Filter

Sometimes, based on the value of a parameter you may need to push data to a relation filter. The push() method does just this. It accepts one argument as an array of key value pairs or to arguments as a key value pair push($key, $value). Related models are filtered AFTER all local values have been executed you can use this method in any filter method. This avoids having to query a related table more than once. For Example:

public $relations = [
    'clients' => ['industry', 'status'],
];

public function statusType($type)
{
    if($type === 'all') {
        $this->push('status', 'all');
    }
}

The above example will pass 'all' to the stats() method on the clients relation of the model.

Calling the push() method in the setup() method will allow you to push values to the input for filter it's called on

Pagination

If you want to paginate your query and keep the url query string without having to use:

{!! $pages->appends(Input::except('page'))->render() !!}

The paginateFilter() and simplePaginateFilter() methods accept the same input as Laravel's paginator and returns the respective paginator.

class UserController extends Controller
{
    public function index(Request $request)
    {
        $users = User::filter($request->all())->paginateFilter();

        return view('users.index', compact('users'));
    }

OR:

    public function simpleIndex(Request $request)
    {
        $users = User::filter($request->all())->paginateSimpleFilter();

        return view('users.index', compact('users'));
    }
}

In your view $users->render() will return pagination links as it normally would but with the original query string with empty input ignored.

Contributing

Any contributions welcome!