moonofmylife/eloquentfilter

以优雅的方式过滤 Eloquent 模型

2.4.3 2021-02-07 01:06 UTC

README

Latest Stable Version Total Downloads Daily Downloads License StyleCI Build Status PHPUnit Status

以优雅的方式过滤 Eloquent 模型及其关系

简介

假设我们想要返回一个根据多个参数过滤的用户列表。当我们访问

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

$request->all() 将返回

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

要过滤所有这些参数,我们需要做类似的事情

<?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();
    }

}

使用 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();
    }

}

配置

通过 Composer 安装

composer require tucker-eric/eloquentfilter

有几种方式可以定义模型将使用的过滤器

默认设置

所有过滤器的默认命名空间是 App\ModelFilters\,每个模型都期望过滤器类名遵循 {$ModelName}Filter 命名约定,无论模型所在的命名空间如何。以下是根据默认命名约定提供的模型及其相应过滤器的示例。

Laravel

使用配置文件(可选)

注册服务提供程序将为您提供访问 php artisan model:filter {model} 命令的权限,并允许您发布配置文件。注册服务提供程序不是必需的,除非您想更改默认命名空间或使用 artisan 命令

安装 Eloquent Filter 库后,在 config/app.php 配置文件中注册 EloquentFilter\ServiceProvider::class

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

    EloquentFilter\ServiceProvider::class,
],

使用发布命令将包配置复制到本地配置

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

config/eloquentfilter.php 配置文件中。设置模型过滤器所在的命名空间

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

Lumen

注册服务提供程序(可选)

这仅当您想使用 php artisan model:filter 命令时才需要。

bootstrap/app.php

$app->register(EloquentFilter\LumenServiceProvider::class);
更改默认命名空间

bootstrap/app.php

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

定义默认模型过滤器(可选)

以下内容是可选的。如果没有找到模型上的 modelFilter 方法,则模型的过滤器类将根据 默认命名约定 解决

在您的模型中创建一个公开方法 modelFilter(),它返回 $this->provideFilter(Your\Model\Filter::class);

<?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
}

动态过滤器

您可以通过将过滤器作为 filter() 方法的第二个参数传递来动态定义过滤器。动态定义过滤器将优先于为模型定义的任何其他过滤器。

<?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();
    }
}

生成过滤器

仅当您已在 `config/app.php` 中将 EloquentFilter\ServiceProvider::class 注册为提供程序数组时才可用

您可以使用以下 artisan 命令创建模型过滤器

php artisan model:filter User

其中 User 是您为创建过滤器而创建的 Eloquent 模型。这将创建 app/ModelFilters/UserFilter.php

该命令还支持 psr-4 命名空间,用于创建过滤器。您只需确保在类名中转义反斜杠。例如

php artisan model:filter AdminFilters\\User

这将创建 app/ModelFilters/AdminFilters/UserFilter.php

用法

定义过滤器逻辑

根据传递给 filter() 方法的 camelCase 输入键定义过滤器逻辑。

  • 忽略空字符串和 null 值
  • 如果定义了 setup() 方法,它将在任何过滤器方法之前被调用一次,无论输入如何
  • _id 从输入键的末尾删除以定义方法,因此过滤 user_id 将使用 user() 方法
    • (可以通过在过滤器上定义 protected $drop_id = false; 来更改)
  • 没有对应过滤器方法的输入将被忽略
  • 键的值将注入到方法中
  • 所有值都可以通过 $this->input() 方法或通过键 $this->input($key) 获取单个值
  • 在模型过滤器类中,所有 Eloquent Builder 方法都可以在 $this 上下文中访问。

为了定义以下输入的方法

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

您将使用以下方法

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);
    }
}

注意:在上面的示例中,如果您不希望从输入的末尾删除 _id,则可以在您的过滤器类上设置 protected $drop_id = false。这样做将允许您拥有一个 company() 过滤器方法以及一个 companyId() 过滤器方法。

注意:在上面的示例中,如果您不希望将 mobile_phone 映射到 mobilePhone(),则可以在您的过滤器类上设置 protected $camel_cased_methods = false。这样做将允许您拥有一个 mobile_phone() 过滤器方法而不是 mobilePhone()。默认情况下,由于以下其中一个输入键:mobile_phonemobilePhonemobile_phone_id,可以调用 mobilePhone() 过滤器方法。

注意:在上面的示例中,所有在 setup() 中定义的方法将在对模型调用 filter() 时每次调用。

黑名单

blacklist 数组中定义的任何方法都不会被过滤器调用。这些方法通常用于内部过滤器逻辑。

可以使用 blacklistMethod()whitelistMethod() 方法动态地黑名单和白名单方法。

在上面的示例中,即使输入数组中有 secret_method 键,也不会调用 secretMethod()。为了调用此方法,它需要被动态白名单。

示例

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

附加过滤器方法

Filterable 特性还提供了以下查询构建器辅助方法

由于这些方法是 Filterable 特性的一部分,因此可以从实现该特性的任何模型中访问它们,而无需在模型的 EloquentFilter 中调用。

将过滤器应用于模型

在任意 Eloquent 模型上实现 EloquentFilter\Filterable 特性

<?php

namespace App;

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

class User extends Model
{
    use Filterable;

    //User Class
}

这使您可以访问接受输入数组作为参数的 filter() 方法

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

通过关系进行过滤

有两种方式可以通过相关模型进行过滤。使用 $relations 数组来定义要注入到相关模型过滤器中的输入。如果相关模型没有自己的模型过滤器,或者您只是想在本地的关系中定义如何过滤而不是将该逻辑添加到该模型的过滤器中,则可以使用 related() 方法通过没有模型过滤器的相关模型进行过滤。您甚至可以组合这两种方法,并定义要在 $relations 数组中使用的输入字段,以使用该模型的过滤器,并使用 related() 方法在该相同的关联上定义本地方法。两种方法都将过滤约束嵌套在相同的关系 whereHas() 查询中。

对于以下示例,我们将使用以下模型

一个 App\User,它 hasMany App\Client::class

class User extends Model
{
    use Filterable;

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

并且每个 App\Client 都属于 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);
    }
}

我们想要查询我们的用户,并按他们过去产生收入的客户的行业和潜在量进行过滤。

用于过滤的输入

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

设置

两种方法都会在每次查询这个关系时调用一个设置查询。设置方法的签名是 {$related}Setup(),并且注入了该关系的查询构建器的实例。对于这个例子,假设当按客户查询用户时,我只想显示有收入的代理商。为了避免在选择方法时没有所有输入而错过整个范围(因为有时我们可能没有所有输入,如果选择了错误的方法,可能会错过整个范围),并且为了避免在所有方法上放置该约束而导致查询重复,我们在 UserFilter 中调用相关设置方法,如下所示:

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

这将把 hasRevenue() 预先附加到 clients() 关系,每次在 UserFilter 运行任何对 clients() 关系的约束时。如果没有对 clients() 关系的查询,则此方法不会调用。

您可以在这里了解更多关于范围的信息

过滤相关模型的方法

使用 related() 方法过滤相关模型

related() 方法设置起来稍微简单一些,如果你不打算使用相关模型过滤器来明确地过滤该模型,这是一个很好的选择。related() 方法接受与 Eloquent\Builderwhere() 方法相同的参数,除了第一个参数是关系名称。

示例

UserFilter 使用 ModelFilterrelated() 方法的一个 industry() 方法

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);
    }
}

或者,您甚至可以将闭包作为第二个参数传递,这将注入相关模型查询构建器的实例,如下所示:

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

使用 $relations 数组过滤相关模型

将关系添加到 $relations 数组中,将模型上引用的关系名称作为键,以及传递给 filter() 方法的输入键数组。

相关模型 必须ModelFilter 关联。我们实例化相关模型的过滤器,并使用 $relations 数组中的输入值来调用相关的方法。

当在关系表的多个列上查询时,这非常有用,同时避免了为同一关系进行多次 whereHas() 调用。对于单列,在模型过滤器中使用 $this->whereHas() 方法就足够了。事实上,在内部,模型过滤器应用了 whereHas() 方法中的所有约束。

示例

UserFilter 定义了关系,因此可以查询。

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

ClientFilter 使用 industry 方法进行过滤

注意: $relations 数组应识别关系和用于过滤该关系的输入键。就像 ModelFilter 一样,这将访问该关系过滤器上的驼峰式方法。如果上述示例使用 industry_type 作为输入键,则关系数组将是 $relations = ['clients' => ['industry_type']],并且 ClientFilter 将有 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 数组别名支持

$relations 数组支持别名。这用于当输入不匹配相关模型过滤器方法时。这将转换传递给相关模型过滤器输入的输入键。

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

上述代码将接收一个数组,如下所示:

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

ClientFilter 将以如下形式接收它:

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

允许使用更描述性的输入名称,而无需过滤器需要匹配。允许更多的过滤器重用。

使用两种方法过滤相关模型

您甚至可以使用这两种方法,并将产生相同的结果,并且只需查询一次相关模型。一个例子如下:

如果以下数组传递给 filter() 方法:

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

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);
        });
    }
}
向过滤器添加关系值

有时,根据参数的值,您可能需要将数据推送到关系过滤器。push()方法正是为此而设计的。它接受一个参数,该参数是一个键值对数组,或者接受两个参数作为键值对push($key, $value)。相关模型在所有本地值执行完毕后进行过滤,您可以在任何过滤方法中使用此方法。这可以避免多次查询相关表。例如

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

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

上述示例将'all'传递给模型中clients关系上的status()方法。

setup()方法中调用push()方法,将允许您将值推送到所调用过滤器的输入

分页

如果您想分页查询并保持URL查询字符串,而不必使用

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

paginateFilter()simplePaginateFilter()方法接受与Laravel的分页器相同的输入,并返回相应的分页器。

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

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

或者

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

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

在您的视图中,$users->render()将返回分页链接,就像通常一样,但忽略原始查询字符串中的空输入。

贡献

欢迎任何贡献!