Laravel 模型过滤(Filter)设计
在我们日常代码开发中,可能最常见的功能就是列表筛选了。通过不同的参数,返回符合条件的内容。下面我分享一下自己的过滤代码设计(其实是从 Laracasts 上学来的?)。
基本实现
假设我们有一个图书的列表,想要进行筛选:
- 标题含有"我们"的
- 定价大于30元的
- 出版社是"大地出版社"的
- 按照 ID 进行倒序排列
针对这几个条件我们可以很快的写出下面的代码
public function index()
{
return Book::query()
->where('title', 'like', '%我们%')
->where('price', '>=', 30)
->where('publisher', '大地出版社')
->orderBy('id', 'DESC')
->get();
}
优化
虽然我们实现了需求,但是如果我们增加或者减少筛选条件呢,以及在请求参数存在的时候才进行过滤呢?我们将不断的在上面的代码中更改,当条件过多的时候,我们的代码将会变成一团乱麻。
下面是我的优化方法。
BaseFilter
首先我们需要一个基础的 QueryFilter,然后我们所有的Filter都继承这个类。
abstract class QueryFilter
{
protected $request;
protected $builder;
public function __construct(Request $request)
{
$this->request = $request;
}
public function apply(Builder $builder)
{
$this->builder = $builder;
foreach ($this->filters() as $name => $value) {
if (method_exists($this, $name)) {
call_user_func_array([$this, $name], array_filter([$value]));
}
}
return $this->builder;
}
public function filters()
{
return $this->request->all();
}
}
这个类的代码很简单,主要功能集中在 apply 函数中,我们检查每个请求参数,如果这个方法存在,那么调用对应的方法。下面结合具体的实例进行解释
BookFilter
我们的 BookFilter 代码如下:
class BookFilter extends QueryFilter
{
public function title($title)
{
return $this->builder->where('title', 'like', "%{$title}%");
}
public function price($price)
{
return $this->builder->where('price', '>=', "%{$price}%");
}
public function publisher($publisher)
{
return $this->builder->where('publisher', $publisher);
}
}
在上面的代码中,我们可以通过 URL 的参数来进行动态查询。
books?title=我们 // 查找标题含有我们的图书
books?title=我们&price=25 // 查找标题含有我们并且价格大于25元的图书
这种结构的代码将会很灵活的控制我们的过滤列表,并且我们的代码也很整洁。
完结
当然,如果我们只进行到这一步是没法进行查询的,因为我们还没有地方调用 QueryFilter 中的 apply 方法。有一个绝佳的地方可以进行调用,那就是模型中的 scope 。
class Book extends Model
{
public function scopeFilter($query, QueryFilter $filters)
{
return $filters->apply($query);
}
}
最后,我们原来控制器的方法将改为下面的样子:
public function index(BookFilter $filters)
{
return Book::filter($filters)->get();
}
这样我们就完成了一个比较灵活的筛选列表了。
PS:不了解 scope 用法的可以看 这里
本作品采用《CC 协议》,转载必须注明作者和本文链接
高认可度评论:
思路很好 和我之前的想法不谋而合 不过我感觉有些地方可以优化一下 粘上代码
BaseFilter.php
AdminsFilter.php
继承
调用
这样做的原因 主要考虑到 大量会复用的 ID name status 之类的
而且使用方法更优雅 直接继承后逻辑页面调用即可
无需再mode 再单独写个方法
不用写 when 了,是个思路
@yybawang 是的,还比较灵活
return $filters->apply($query);
思路很好 和我之前的想法不谋而合 不过我感觉有些地方可以优化一下 粘上代码
BaseFilter.php
AdminsFilter.php
继承
调用
这样做的原因 主要考虑到 大量会复用的 ID name status 之类的
而且使用方法更优雅 直接继承后逻辑页面调用即可
无需再mode 再单独写个方法
用when好,还是这个好呢?
@13122826258 简单使用的话直接用when就可以
public function index(BookFilter $filters)
{
return Book::filter($filters)->get();
}
这里调用的这个参数 Book::filter($filters)->get();是不是不对,我怎么没看懂呢
@wojianduanfa_sxm_87
这里定义了scopeFilter,可以直接用filter调用,然后apply返回$this->builder可以连续调用
@wojianduanfa_sxm_87 可以看一下我文章最后面的链接,讲解的是scope的用法
scope 就是 laravel 里的作用域吧?
@Savory 对
版主大人,copy 了你的代码后,我不知道咋用,比如,我想做这种查询: where('title', 'like', '%我们%'),where('price', '>=', 30) 的这种操作,控制器里面需要怎么写?
还有 abstract class QueryFilter 中的 Builder 是 use Illuminate\Database\Eloquent\Builder; 么? Request 是 use Illuminate\Http\Request; 么?
@Savory 是的,写法和我文章里的基本一致,用法的话就是通过访问地址来处理的。例如
主要是通过request请求参数来处理
@Epona OKOK了,谢谢版主大人,懂了,还在奇怪,既然用了 Request,需要在哪里传参呢
@Savory 这里主要是进行动态搜索的,如果是有一些固定的查询,就没必要这么费事了。
@Epona 原来版主大人的这个搜索查询,是把原来的 scope 进行了升级,代码可读性更高了,学习了。若把它和 repository 模式结合,估计质量更棒吧?
有这个包tucker-eric/eloquentfilter
@半人间 一个文件夹、 两个类就能解决的事情去使用包,反而与优雅使用where条件的初衷相违背
@laaa 多个地方使用的话,可以考虑使用。只有一两个地方用是没什么必要的。
假如你这个book模型存的只是书的名字,还有一个author存的是书的作者和所有为这个书作序的其他作者,那么如何查找小明是不是这本书的作者呢,即用Book::filter($filters)->get();查找的只能是Book表中存在的字段,如何还能查找author表中的字段呢
@xiucai 这个也可以查找对应的关联关系
大概是这么一种写法吧
查不到name是因为最终sql是这样的
select from books where exists ( select from authors where books.id = authors.book_id and authors.name='唐家三少')
whereHas用的是exists
问题一:如果我想同时查找书名和作者改怎么写呢
书籍表:
DROP TABLE IF EXISTS
books
;CREATE TABLE
books
(id
int(11) NOT NULL AUTO_INCREMENT,title
varchar(100) NOT NULL DEFAULT '' COMMENT '书名',PRIMARY KEY (
id
)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
INSERT INTO
books
VALUES ('1', '斗罗大陆一');INSERT INTO
books
VALUES ('2', '武动乾坤');INSERT INTO
books
VALUES ('3', '完美世界');作家表:
DROP TABLE IF EXISTS
authors
;CREATE TABLE
authors
(id
int(11) NOT NULL AUTO_INCREMENT,book_id
int(11) NOT NULL DEFAULT '0',name
varchar(50) NOT NULL DEFAULT '' COMMENT '姓名',identity
tinyint(4) NOT NULL DEFAULT '1' COMMENT '1:作者 2:作序者',PRIMARY KEY (
id
)) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='作家';
INSERT INTO
authors
VALUES ('1', '1', '唐家三少', '1');INSERT INTO
authors
VALUES ('2', '2', '天蚕土豆', '1');INSERT INTO
authors
VALUES ('3', '3', '辰东', '1');INSERT INTO
authors
VALUES ('4', '1', '老书虫', '2');INSERT INTO
authors
VALUES ('5', '1', '颜如玉', '2');出版社表:
DROP TABLE IF EXISTS
presses
;CREATE TABLE
presses
(id
int(11) NOT NULL AUTO_INCREMENT,name
varchar(50) NOT NULL DEFAULT '' COMMENT '出版社名称',PRIMARY KEY (
id
)) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT='出版社';
INSERT INTO
presses
VALUES ('1', '北京出版社');INSERT INTO
presses
VALUES ('2', '长春出版社');INSERT INTO
presses
VALUES ('3', '重庆出版社');INSERT INTO
presses
VALUES ('4', '湖南人民出版社');出版社和数据关联表:
DROP TABLE IF EXISTS
press_has_books
;CREATE TABLE
press_has_books
(id
int(11) NOT NULL AUTO_INCREMENT,press_id
int(11) NOT NULL DEFAULT '0',book_id
int(11) NOT NULL DEFAULT '0',PRIMARY KEY (
id
)) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='出版社和书籍关联表';
INSERT INTO
press_has_books
VALUES ('1', '1', '1');INSERT INTO
press_has_books
VALUES ('2', '1', '2');INSERT INTO
press_has_books
VALUES ('3', '4', '3');问题二:通过Book::filter($filters)->get();获取作者为唐家三少的书籍信息并且同时列出作者和对应的出版社,其实问题二是问题一的加强版 :joy:
楼主别怪我问题多呀,刚学习laravel觉得你这个模型过滤查询挺不错的,但是我处理不了所要面临的上面几个问题,烦请大佬指点小弟一二 :sob:
@xiucai 结果列没有 name 是你需要返回name,可以试试
应该就会返回作者信息,出版社也同理
当两个参数 相互制约的情况下呢?
这个在社区的 测试驱动开发教程 里看过,
我这稍微改进了一点点,,,具体逻辑就不贴代码了,
大概就是, 加了一种 "简单查询" 的规则, 一些可以用
$query->where('field', 'op', 'value')
的查询 不用每个字段都定义一个方法,比如在
PermissionFilter
中定义一个属性:那要是想要
where('name', '!=', 'xxx')
这样呢?@翁航 在方法里写就可以了
@Epona 如果一本图书和分类属于多对多的关系,并且我想结合分类查找 标题含有 "我们" 的, 应该怎么写
@____ 新写一个方法就行
在超哥的开源项目 yike.io 中,我发现使用了扩展包 EloquentFilter ,推荐使用。
call_user_func_array([$this, $name], array_filter([$value])); 如果 $value是空值的话 , 那他执行那个方法就获取不到参数 。 会报错
被判断成
null
而删除了。那就改呗,
null
不用array_filter
就好了。