tucker-eric/eloquentfilter

以优雅的方式筛选 Eloquent 模型

安装: 3,479,687

依赖: 24

建议者: 1

安全: 0

星星: 1,717

关注者: 42

分支: 121

开放问题: 2

3.4.0 2024-05-07 20:28 UTC

README

Latest Stable Version Total Downloads Daily Downloads License StyleCI 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 Filter 筛选相同的输入

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

生成筛选器

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

您可以使用以下 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() 方法的驼峰式输入键定义过滤逻辑。

  • 默认情况下,空字符串和null值会被忽略。
    • 可以通过在过滤器上设置 protected $allowedEmptyFilters = false; 来配置空字符串和值不被忽略。
  • 如果定义了 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()。默认情况下,可以通过以下输入键调用 mobilePhone() 过滤方法:mobile_phonemobilePhonemobile_phone_id

注意:在上面的示例中,所有 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();
    }
}

这将在 UserFilter 运行对 clients() 关系的任何约束时,将 hasRevenue() 预先添加到 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' 传递给模型 clientsstatus() 方法。

setup() 方法中调用 push() 方法将允许您向筛选器输入推送值

分页

如果您想分页查询并保留 URL 查询字符串,而无需使用

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

paginateFilter()simplePaginateFilter() 方法接受与 Laravel 的分页器相同的输入 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() 将返回分页链接,就像通常一样,但是如果有空输入且未设置 protected $allowedEmptyFiltersfalse,则忽略原始查询字符串。

贡献

欢迎任何贡献!