GithubHelp home page GithubHelp logo

tucker-eric / eloquentfilter Goto Github PK

View Code? Open in Web Editor NEW
1.7K 42.0 117.0 361 KB

An Eloquent Way To Filter Laravel Models And Their Relationships

Home Page: http://tucker-eric.github.io/EloquentFilter

License: MIT License

PHP 100.00%
model-filters eloquent-filters dynamic-filters relation-filter laravel eloquent filter-logic eloquent-models filter query

eloquentfilter's Introduction

Eloquent Filter

Latest Stable Version Total Downloads Daily Downloads License StyleCI PHPUnit 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

Laravel

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\\",

Lumen

Register The Service Provider (Optional)

This is only required if you want to use the php artisan model:filter command.

In bootstrap/app.php:

$app->register(EloquentFilter\LumenServiceProvider::class);
Change The Default Namespace

In bootstrap/app.php:

config(['eloquentfilter.namespace' => "App\\Models\\ModelFilters\\"]);

Define The Default Model Filter (optional)

The following is optional. If no modelFilter method is found on the model the model's filter class will be resolved by the default naming conventions

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 and null values are ignored
  • If a setup() method is defined it will be called once before any filter methods regardless of input
  • _id is dropped from the end of the input key to define the method so filtering user_id would use the user() method
    • (can be changed with by definining protected $drop_id = false; on a filter)
  • 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:

use EloquentFilter\ModelFilter;

class UserFilter extends ModelFilter
{
    protected $blacklist = ['secretMethod'];
    
    // 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();
        }
    }
    
    public function secretMethod($secretParameter)
    {
        return $this->where('some_column', true);
    }
}

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 above example if you do not want mobile_phone to be mapped to mobilePhone() you can set protected $camel_cased_methods = false on your filter class. Doing this would allow you to have a mobile_phone() filter method instead of mobilePhone(). By default, mobilePhone() filter method can be called thanks to one of the following input key: mobile_phone, mobilePhone, mobile_phone_id

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

Blacklist

Any methods defined in the blackist array will not be called by the filter. Those methods are normally used for internal filter logic.

The blacklistMethod() and whitelistMethod() methods can be used to dynamically blacklist and whitelist methods.

In the example above secretMethod() will not be called, even if there is a secret_method key in the input array. In order to call this method it would need to be whitelisted dynamically:

Example:

public function setup()
{
    if(Auth::user()->isAdmin()) {
        $this->whitelistMethod('secretMethod');
    }
}

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->whereLike($column, $string, 'or') $query->orWhere($column, 'LIKE', '%'.$string.'%')
$this->whereBeginsWith($column, $string) $query->where($column, 'LIKE', $string.'%')
$this->whereBeginsWith($column, $string, 'or') $query->orWhere($column, 'LIKE', $string.'%')
$this->whereEndsWith($column, $string) $query->where($column, 'LIKE', '%'.$string)
$this->whereEndsWith($column, $string, 'or') $query->orWhere($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 will prepend the query to the clients() relation with 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 multiple 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);
    }
}
$relations array alias support

The $relations array supports aliases. This is used when the input doesn't match the related model's filter method. This will transform the input keys being passed to the related model filter's input.

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

The above will receive an array like:

[
    'client_industry'  => 1,
    'client_potential' => 100000
]

And the ClientFilter will receive it as:

[
    'industry'         => 1,
    'potential_volume' => 100000
]

Allowing for more descriptive input names without filters needing to match. Allowing for more reuse of the same filters.

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 two 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 status() 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())->simplePaginateFilter();

        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 are welcome!

eloquentfilter's People

Contributors

aurewill avatar bitdeli-chef avatar bobbypiper avatar dacoto97 avatar dependabot[bot] avatar designvoid avatar droplister avatar her-cat avatar hungkin avatar imanolympic avatar kanidjar avatar levitarr avatar lorenzosapora avatar mouadziani avatar mrwogu avatar owenvoke avatar pushpak avatar serderovsh avatar snowlyg avatar specialtactics avatar tucker-eric avatar yoyomo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

eloquentfilter's Issues

Improvement

I think it will be useful if the filterInput function changed as following,

ModelFilter.php

    public function filterInput()
    {
        foreach ($this->input as $key => $val) {
            // Call all local methods on filter
            $method = $this->getFilterMethod($key);

            if (method_exists($this, $method)) {
               /* $this->{$method}($val);*/
               $this->{$method}($val ,$this->input );

            }
        }
    }

then the example filter function would like bellow. inside that function, for an example, we can write any condition that id depends on other args parameters

class UserFilter

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

Using Passport with EloquentFilter

Hi Eric,

Just a quick question, can Laravel Passport work with EloquentFilter and how do you go about it?

Thanks a lot for any input.

Regards,

Junel

Fuzzy searching

Hi Eric,

First, just want to thank you for making this wonderful piece of software.

Just want to ask for insights on how to implement fuzzy searching within the defined filters.

Thanks in advance!

Regards,

Junel

No Method Name Validation or Whitelist

At the moment, no validation or normalisation runs in the filterInput function. i.e. any key present in the input array will call a function with that name. This means any method can be called arbitrarily. For example, using the implementation as discussed in the readme, if one defines any (public or protected) function (e.g. superSecretMethod), then this could be called by visiting the appropriate endpoint with 'super_secret_method' as a query argument.

This is unlikely to cause a major security issue, since no actions should really be defined within a Filter, and the output of the function is never returned to the user, however I don't think arbitrary method calls are a good idea.

Some possible solutions:

  • create a whitelist of possible filters (i.e. require a parameter or function to be defined which specifies all the available filters)
  • come up with a common naming scheme for filters (e.g. get{FILTER_NAME}Filter, à la getFooAttribute)

/**
* Filter with input array.
*/
public function filterInput()
{
foreach ($this->input as $key => $val) {
// Call all local methods on filter
$method = $this->getFilterMethod($key);
if (method_exists($this, $method)) {
$this->{$method}($val);
}
}
}

Nested where not working

This is my filter:

public function search($search)
	{
		return $this->where(function($q) use ($search)
		{
			return $q->where('description', 'LIKE', "%$search%")
			         ->orWhere('title', 'LIKE', "%$search%");
		});
	}

But this is the query that's being run:

select `listings`.*, (select count(*) from `media` where `listings`.`id` = `media`.`model_id` and `media`.`model_type` = 'App\Listing') as `media_count` from `listings` where (`description` LIKE 'App\Listing' or `title` LIKE '%a%') and `category_id` = '%a%' limit 2 offset 0

For some reason, the first instance of $search is returning the classname of the model instead of the search term that's being passed through the query string. I'm unsure if I've just set this up incorrect or if it's a bug.

how to filter belongsToMany relation

how to filter belongsToMany relation ?

TalentFilter :

public function skills($ids)
{
    is_string($ids) ? $ids = explode(',', $ids) : $ids;

    $this->related('skills', function($query) use ($ids) {
        return $query->whereIn('skill_id', $ids);
    });

}

Talent Model :

public function skills()
{
    return $this->belongsToMany(Skill::class);
}

Return only certain fields

Hi again,
Is it possible to return just some selected fields? Like User::get(['first_name','last_name','email']);
There are some cases when I want to hide some fields when searching (like a public search) and I cannot use the protected $hidden attribute to specify those fields because in other situations I need those fields to be shown by default.
Thanks for this awesome package.
Lilian

config directory changed

when I try to create a config file, artisan creates in the app folder, whereas the readme states:

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

I tried a second time to run the command to make sure (using the command in readme - php artisan model:filter User) and got:

php artisan model:filter User

/Users/robbielove/dev/xxxxxx/app/UserFilter.php Already Exists!

this is starting to really confuse me about where the file should be located, in saying this - i have tried using it in both app/UserFilter.php and app/ModelFilters/UserFilter.php and still get variations of

Class 'App\ModelFilters\UserFilter' not found

the newly generated UserFilter contains:


use EloquentFilter\ModelFilter;

class UserFilter extends ModelFilter
{
    /**
    * Related Models that have ModelFilters as well as the method on the ModelFilter
    * As [relatedModel => [input_key1, input_key2]].
    *
    * @var array
    */
    public $relations = [];
}

I have tried with and without adding modelFilter() to my user model. nothing I do seems to get it working, I tried with just an orderby() method in-case my code was causing the class not to be created, but still same error.

Arrays as multiple filters

It seems that utilizing arrays does not account for the case that you may want to apply the same filter multiple times. Instead of calling the filter once with an array as the input, how can we achieve calling the filter multiple times with each input being one item in the array?

Ability to order results

Hi,
This is a great package and it is really helping my project.
One thing I miss is the ability to order results based on query parameters.

Any thoughts?

1.3.2 update breaks

HI,

I updated to version 1.3.2 and i have this error:

FatalErrorException in ModelFilter.php line 571:
Call to undefined method Illuminate\Database\Eloquent\Relations\Relation::hasMacro()
in ModelFilter.php line 571

at Application->handleShutdown() in RegistersExceptionHandlers.php line 55
at Application->Laravel\Lumen\Concerns{closure}()

forcing the version to 1.3.1 in composer.json solve the error.

How to execute laravel WHERE clause along with EloquentFilter

When i use this controller it works

public function indexArtisan(Request $request)
{
    $workers = Worker::filter($request->all())->get();
    $states = DB::table('states')->pluck("name", "id");
    return view ('artisans', compact('states'))->with(['workers' => $workers]);
}

but when i use this

public function indexArtisan(Request $request)
{
    $work = DB::table('workers')->where('career', '=', 'artisan')->get();
    $workers = Worker::filter($request->all())->get();
    $states = DB::table('states')->pluck("name", "id");
    return view ('artisans', compact('states','work'))->with(['workers' => $workers]);
}

it doesnt work. Any ideas on what to do?

Just a heads up

I just did a test upgrade to L5.4 and your package caused a few errors. I'm not going to be working on the latest version for my current project so don't have time at the mo to fix and submit a PR but figured I'd give you a heads up!

Vendor publish is not doing anything

Since the update, I am not able to publish the configuration file with a new laravel installation.

I have the service provider in my app.php, but its not doing anything.

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

Filtering on another database table

I am struggling to create a filter that uses a table "content_features" from another database "sac_repo_db".

If the user doesn’t select a filter the query is created correctly:

SELECT 'purchased_contents'.'id', 'purchased_contents'.'title', 'purchased_contents'.'description', 'sac_repo_db'.'content_features'.'content_type' FROM 'purchased_contents' inner join 'sac_repo_db'.'content_features' on 'purchased_contents'.'content_feature_id' = 'sac_repo_db'.'content_features'.'id' WHERE 'content_type_id' > '0'

However when a filter is applied the query omits the reference to the other database:

select 'purchased_contents'.'id', 'purchased_contents'.'title', 'purchased_contents'.'description', 'sac_repo_db'.'content_features'.'content_type' from 'purchased_contents' inner join 'sac_repo_db'.'content_features' on 'purchased_contents'.'content_feature_id' = 'sac_repo_db'.'content_features'.'id' where exists (select * from 'content_features' where 'purchased_contents'.'content_feature_id' = 'content_features'.'id' and 'content_type_id' in (6))

What it should create is this:
select 'purchased_contents'.'id', 'purchased_contents'.'title', 'purchased_contents'.'description', 'sac_repo_db'.'content_features'.'content_type' from 'purchased_contents' inner join 'sac_repo_db'.'content_features' on 'purchased_contents'.'content_feature_id' = 'sac_repo_db'.'content_features'.'id' where exists (select * from 'sac_repo_db'.'content_features' where 'purchased_contents'.'content_feature_id' = 'sac_repo_db'.'content_features'.'id' and 'content_type_id' in (6))

I realise this is rather a complex question, but I think I am probably missing something fairly simple to get the filter to construct the correct query. It works fine if I use
Any help would much appreciated.

PurchasedContentTableController.php

<?php

namespace App\Http\Controllers\Admin;

use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Cache;
use Illuminate\Http\Request;
use App\ModelFilters\PurchasedContentFilter;
use App\PurchasedContent;


class PurchasedContentTableController extends Controller
{
    public function filteredIndex(Request $request)
    {
        return PurchasedContent
            ::select('purchased_contents.id', 'purchased_contents.title', 'purchased_contents.description', 'sac_repo_db.content_features.content_type')
            ->join('sac_repo_db.content_features', 'purchased_contents.content_feature_id', '=', 'sac_repo_db.content_features.id')
            ->filter($request->all())->get();
    }
}

PurchasedContentFilter.php

<?php
namespace App\ModelFilters;

use EloquentFilter\ModelFilter;

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

    public function contentFeatures($content_type_ids)
    {        
        if ( count($content_type_ids) > 0 ) {
            return $this->whereHas('contentFeature', function($query) use ($content_type_ids)
            {
                return $query->whereIn('content_type_id', $content_type_ids);
            });
        }

        return $this->where('content_type_id', '>', 0);
    }
}

PurchasedContent.php

<?php

namespace App;

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

class PurchasedContent extends Model
{

    use Filterable;

    protected $guarded = ['id', 'created_at', 'updated_at'];

    protected $dates = ['purchased_at', 'created_at', 'updated_at'];

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

    public function contentFeature()
    {
        return $this->belongsTo('App\RepoModels\ContentFeature');
    }
}

ContentFeature.php

<?php

namespace App\RepoModels;

use Illuminate\Database\Eloquent\Model;

class ContentFeature extends Model
{
    
    protected $connection = 'mysql_repo';

    protected $guarded = ['id', 'created_at', 'updated_at'];

    protected $dates = ['created_at', 'updated_at'];


    public function contents()
    {
        return $this->hasMany(Content::class);
    }
}

There is a way to filter a related model which have the same fields ?

Hi Eric:
Thanks for your excellence lib.
just like I mention in the title,
what if the Client model and the User model both have a filed named 'title',

class UserFilter extends ModelFilter
{
    public $relations = [
        'clients' => ['title', 'potential_volume'],
    ];
}
public function title($title)
{
   return $this->whereLike('title', $title);
}
}

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

 public function title($title)
{
   return $this->whereLike('title', $title);
}
    
}

How can they work?

Filters only effect initial query ... expected behaviour?

Hello again,

I've got (perhaps) an issue but I'm just hoping to conclude this is the expected behaviour or something is wrong / missing in my codebase.

Consider this combination of ModelFilter example.

class ParentFilter extends ModelFilter
{
    public $relations = [
        'childModel' => ['active']
    ];
}

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

    public function active($value) {
        return $this->where('active', $value);
    }
}

On the initial query it creates an "exist" sub-query and checks it can find atleast 1 child which is 'active'.

select * from `parent_table`
where `uuid` = ?
and exists
    (select * from `children_table`
    where `parent_table`.`parent_id` = `children_table`.`parent_id`
    and `active` = ?
    and `children_table`.`deleted_at` is null)
and `parent_table`.`deleted_at` is null
(
    [0] => 3f151f53-935a-4577-af5b-ed6669524ad3
    [1] => 1
    [2] => 1
)

Then inevitably my code attempts to grab the children from the parent (e.g. ParentTable->childrenTable). And the code runs this query:

select * from `children_table`
where `children_table`.`parent_id` in (?)
and `children_table`.`deleted_at` is null
(
    [0] => 601
)

Notice how the second query does not have an 'active' = 1 where clause.

Is this the expected behaviour or do I need to take an additional steps to ensure the ParentTable->childrenTable is properly filtered ( could this relate to the Model::with method? ).

Thank you in advance and for also promptly answering my other issue yesterday.

Using the same $relations values across associated ModelFilters causes PHP Memory Allocation Error

If I setup 2 ModelFilters that pass data using the $relations class property I it causes an infinite loop where it keeps passing data between each ModelFilter (unconfirmed). The error being returned is "PHP Fatal error: Allowed memory size of 134217728 bytes exhausted".

I can use different key names (like a_active, b_active). I could also use the other method for relationships but ideally I'd just like to pass 'active'.

For example consider these ModelFilters which would be related to 2 Models both with a relationship with one another.

class A extends ModelFilter
{
    public $relations = [
        'b' => ['active']
    ];

    public function active() {
        return $this->where('active', $value);
    }
}

class B extends ModelFilter
{
    public $relations = [
        'a' => ['active']
    ];

    public function active() {
        return $this->where('active', $value);
    }
}

Any ideas?

Logical operator OR instead of AND

Hello guys,
Very nice package, I'm coming from a RoR environment where filters are there out of the box, I really needed them here also.
My issue is more like a question: is it possible to use the OR logical operator between queries instead of AND?

Thank you,
Lilian

How to set relation with namespace?

I have two Models.

class Member extends Authenticatable
{
    use Notifiable;
    use EloquentImageMutatorTrait;
    use Filterable;

    public function modelFilter()
    {
        return $this->provideFilter(MemberFilter::class);
    }
    public function evaluation ()
    {
        return $this->hasOne('App\MemberEvaluation','member_id','id');
    }
}

class MemberEvaluation extends Model
{
    use Filterable;

    protected $fillable = [
        'member_id',
        'score',
    ];

    public function modelFilter()
    {
        return $this->provideFilter(MemberEvaluationFilter::class);
    }
}

And I made two filters with namespace:

namespace App\ModelFilters\MemberFilters;

use EloquentFilter\ModelFilter;

class MemberFilter extends ModelFilter
{
    public $relations = [
        'MemberFilters\MemberEvaluation' => ['score_min'],
    ];
}

use EloquentFilter\ModelFilter;

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

    public function scoreMin($value)
    {
        return $this->where('score', '>=', $value);
    }
}

But it will get this error,

Call to undefined method Illuminate\Database\Query\Builder::MemberFilters\MemberEvaluation()

How can I set $relations in MemberFilter?

OR relation

Hi there, is it possible to add relation conditions using OR not AND?
I have:

        $this->where('name', 'LIKE', '%' . $text . '%')
            ->orWhere('excerpt', 'LIKE', '%' . $text.'%')
            ->orWhere('details', 'LIKE', '%' . $text.'%');
        $this->related('format', 'name', 'LIKE', '%' . $text.'%');
        $this->related('primaryFormat', 'name', 'LIKE', '%' . $text.'%');
        $this->related('meta', function ($query) use ($text) {
            return $query->where('key', 'film_synopsis')->where('value', 'LIKE', '%' . $text.'%');
        });

Which add the relation conditions on but as AND and it would like it all to OR... Hope that makes sense?
Cheers!

Dynamic namespace for modular app

Is there a way I can set the namespace dynamically for a modular app?

I am currently using the nwidart/laravel-modules package to separate my application. Some of my modules have a their own separate search facility so the question here is, is there a way I can the namespace for each module dynamically?

THanks

Using OR relation

I read the previous issues regarding using an OR relation - I've been able to make this work. The issue I've run into is applying the orWhere() only after the first filter. I'm including an attribute in the request to indicate whether or not the user would like all the filters to apply as OR or AND. However in order to make this work, the first filter needs to be a where() and the subsequent ones should use orWhere().

Relationship ID based filtering

First off, this package is amazing, appreciate all the hardwork! Really is helping me out.

I'm trying to using filtering on top of api endpoints for specific models. For example I would call this endpoint:
/api/instructor/10/classes

That endpoint would pull only the classes associated with that instructor, but I want to be able to add filter such as:
/api/instructor/10/classes?start=2018-08-15&end=2018-08-17

My code currently for the api endpoint controller looks like this:

public function index($id)
{
        $instructor = Instructor::findOrFail($id);
        $classes = $instructor->classes();

        return (new InstructorClassesResource($classes))->response()->setStatusCode(200);
 }

But wondering how would I implement the filter on top of the findOrFail by id? Would I have to pass a parameter to the setup function?

Would appreciate guidance! Thank you

BelongsToMany and paginateFilter() destroys pivot

When using the method paginateFilter() on a model with pivot table, this pivot table is not present inside the model. When changing back to paginate() the pivot model is back.

companies - company_user - users

In this case, if I would get all the users inside a company, and there would be any additional data inside the pivot table, it would be added inside the 'relations' object inside the model, but this is not present.

Filter with paginator

Really, really good package, using it in a big project now and i'm loving it.

One thing I would like to notice, that you might want to add to the documentation, is the paginator.

When you render the default laravel paginator, the GET values in the URL will drop when you switch to another page, I have resolved this by rendering the paginator this way:

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

This would generate URL for example:

app.com/pages?title=PageTitle&page=3

(Otherwise, the paginator would override the filter options and clear it from the url so you would loose your filter)

Dot notation in key name

I'm passing through the following search criteria, but it's only searching by my name "Ben", the company name is being ignored. How do I use dot notation?

[
    'Name'  => 'Ben',
    'Company.Name'  => 'Nike',
]

Here's my PHP code:

// works
public function name($value)
{
    return $this->where('Name', 'LIKE', "%$value%");
}

// ignored
public function companyName($value)
{
    return $this->related('company', 'Name', 'LIKE', "%$value%");
}

how handle request contain array

In my filter request contain array such as address[region] and address[city] ..
and my ModelFilter how handle these input and write query?

Does this work with Laravel 5.2 or is there a tag I can use that does?

My app is Laravel 5.2 and so far all calls to Model::filter($request->input()) are returning all the models.

Notice I have to use $request->input() as $request->all() does not work in Laravel 5.2.

If there is a version of EloquentFilter that works with Laravel 5.2 please let me know or if it is known to require a higher version of Laravel.

thanks.

[ASK] problem with column prefix

i have success use this lib with normal table,

so i have table, with prefix column, how to define primary column or how to change default sort column

How can I drop _id in the name of my column

screen shot 2018-04-12 at 10 14 45

Hi Eric congratulations for your cool lib, I've had a little problem with the name of the column 'Propiedad.comuna_id' because in my table its name is 'Propiedad.comuna' , How can I resolve this issue, I've read that $drop_id=false in my model filter can be drop _id but I think that I understood bad.
if I change its name to 'propiedad.comuna_id', it works fine, but I have advanced my project and I can't change all the tables.

thanks for your help.

Filtering polymorphic relation

Hi there,

First, thanks for this package as it ease the filtering process nicely!

Second, I've got the following case and require assistance :
A Bill or Invoice if you prefer:

class Bill extends Model
{
    use Filterable;

    public function modelFilter()
    {
        return $this->provideFilter(BillFilter::class);
    }

    public function billable()
    {
        return $this->morphTo();
    }
}

Links to a "billable" something that may have a bill, for the example let's say an order that's been place by a customer:

class Order extends Model
{
    use Filterable;

    public function modelFilter()
    {
        return $this->provideFilter(OrderFilter::class);
    }
   
    public function buyer()
    {
        return $this->belongsTo(Customer::class, 'buyer_id');
    }

    public function bill()
    {
        return $this->morphOne(Bill::class, 'billable');
    }
}

As you can see the order reference a buyer which I would like to filter on via its name when filtering a bill list.

As the Customer filter already exists I tried with the $relations array as so in BillFilter:

    public $relations = [
        'billable' => [ 'customer' ],
    ];

and in OrderFilter I've got the following filter method:

    public function customer($name)
    {
        return $this->whereLike('name', $name);
    }

Thing is it seems that the filterRelations methods goes into an infinite loop in that case. Other test I did was create a "normal" relationship on Bill as so to try if the $relations was setup right:

    public function order()
    {
        return $this->belongsTo(Order::class, 'billable_id');
    }

Which worked as expected.

Would it be possible to filter with the polymorphic relation or not ? And am I doing it wrong ?

is there a way to specify which query parameters to use when using paginateFilter()

I have a situation where I am passing an array of values to filter() which includes a "sorting" parameter

but it so happens that my "sorting" parameter is not a url query parameter, instead it's part of my route

So my route looks like /submissions/latest?page=2

And I pass

[
    "sorting" => $routeParams["sorting"],
    "page" => $queryParams["page"],
]

The filter does its job properly, but then because I'm using paginateFilter

The url produced by the paginator is /submissions/latest?sorting=latest&page=3

How can I tell paginateFilter to not use everything passed to the filter() but only use parameters I whitelist?

Enhancement request / question: Eloquent orderBy option

Hi Eric.
Thanks for this powerful Filter tool!
Is there a possibility to add an orderBy option to the filter?

Something like this:

?title=lorem&sort:title&direction:asc

Which automatically adds ->orderBy('title', 'asc') to the filter query.

Kind regards
Mathias

does not work with hybridrelations between mysql and mongodb relationships

I know this is probably outside the scope of your project but I think it would be really cool if I could get it to work.

I'm using https://github.com/jenssegers/laravel-mongodb which supports hybrid relations between mysql and mongodb models.

I am able to do this no problem User::with('profile')->all();

Where User is a mysql model and Profile is a mongodb model

Furthermore your Filterable trait works out of the box on the mongodb model which is great and I am having a lot of enjoyment using it!

so $users = User::filter($request->get()) and $profiles = Profile::filter($request->get()) both work great.

But if I try to do $users = User::filter(['gender' => 'male'])->get(); then I get errors and the errors are basically showing that it is looking for the profile fields inside mysql instead of making those queries on the mongodb collection.

I've set up my models properly

My User model uses a UserFilter which has

    public function gender($value)
    {
        return $this->where('gender', $value);
    }

And my ProfileFilter has

    public function gender($value)
    {
        return $this->where('gender', $value);
    }

And I've confirmed that the gender function above DOES get called. But when it does I get this error

SQLSTATE[42000]: Syntax error or access violation: 1064 You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'exists ? and `user_id` <> ? and `main_picture` is not null and `gender` = ?)' at line 1 (SQL: select * from `users` where exists (select `user_id` from `profile` where `user_id` exists 1 and `user_id` <> 201 and `main_picture` is not null and `gender` = male))

Basically it's ignoring my HybridRelations setup and assuming that Profile is another mysql model.

The fact that your EloquentFilter DOES work on my mongodb model when used without a relation tells me that this can probably be made to work without too much difficulty.

Is there any chance you could add this functionality or put me in the right direction?

I have a feeling part of the answer might lie in https://github.com/jenssegers/laravel-mongodb/blob/master/src/Jenssegers/Mongodb/Eloquent/HybridRelations.php

Many thanks!

Use filter set up in relationship's filter

I'm trying to filter my cases model by patient, in which both have their own filters respectively. In my patient filter, I have the following filter I'd like to re-use if possible:

class PatientFilter extends ModelFilter
{
    public function name($name)
    {
        return $this->where('first_name', 'LIKE', DB::getPdo()->quote($name . '%'))
            ->orWhere('last_name', 'LIKE', DB::getPdo()->quote('%' . $name . '%'))
            ->orWhereRaw('concat([first_name], \' \', [last_name]) LIKE ' . DB::getPdo()->quote('%' . $name . '%'));
    }
}

I'm trying to access this filter from within my CaseEntityFilter class so I can pass something like patient_name as a filter on the case and return cases matching the patient name.

Here's what I've tried for the filter on cases, but to no luck:

class CaseEntityFilter extends ModelFilter
{
    // Cant seem to get this to point to the method on the actual filter
    public $relations = [
        'patient' => [
            'name',
            'firstName',
            'patient_name'
        ]
    ];

    // Produces 'invalid column' error
    public function patientName($name)
    {
        return $this->related('patient', 'name', $name);
    }

My database (SQL Server) throws an error about the column name not existing (which it rightfully doesn't). I can't seem to figure out how to execute the name() method on my patient's filter instead of just querying for the column 'name'.

I'm new to Laravel so is there something I'm missing or would this be considered a feature request?

Thank you for the excellent plugin by the way! :)

Using different comparisons

Is there a recommended way to use different comparison operators? For example, if I wanted to filter potential_volume is <, > or =. In the examples in the documentation, the comparison operator is implied in the function. Perhaps something like this:

[
  'potential_volume' => '>100',
  'name' => 'james%'
]

Filter by aggregations

Hi.

Somhow I couldn't find any info on the topic of filtering by aggregates, so here's my quesion. I have two models: Group -{has many}-> Student. I would like to filter all groups with average student age more then Y (age is a calculated field, e.g. AVG(TIMESTAMPDIFF(YEAR, person.dob, NOW())) AS age)

I am pretty new to Laravel in general, so sorry if this is not a question about filters exactly.

Thanks in advance for your help.

Model scope methods are not working well

I have a strange issue somehow scope methods are giving strange results.

In User model there is scope function with name active

    public static function scopeActive()
    {
        return static::whereIsActive(1);
    }

So if I do

 User::active()->filter($request->all())->latest()->paginate(20);

In Filter Class

    public function q($username)
    {
        return $this->whereUsername($username); // No Additional check is set
    }

I also get User which are not active.

But

If I do

User::filter($request->all())->latest()->paginate(20);

In filter class

    public function q($username)
    {
        return $this->whereIsActive(1)->whereUsername($username); // Additional check is set
    }

Results are correct.

Does this support lumen 5.4 ?

Does this support lumen 5.4 ?

i get error

PHP error:  Call to undefined function EloquentFilter\config_path() in i/vendor/tucker-eric/eloquentfilter/src/ServiceProvider.php on line 17

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.