错别字:神器 --> 神奇
修改理由:
此投稿已在 6年前 合并。
内容修改:
| Old | New | Differences |
|---|---|---|
| 1 | ||
| 2 | 1 |  |
| 3 | 2 | |
| 4 | 3 | 最近,我需要在开发的事件管理系统中实现搜索功能。 一开始只是简单的几个选项 (通过名称,邮箱等搜索),到后面参数变得越来越多。 |
| 5 | 4 | |
| 6 | 5 | 今天,我会介绍整个过程以及如何构建灵活且可扩展的搜索系统。如果你想查看代码,请访问 [Git 仓库](https://github.com/drawmyattention/advanced-eloquent-search-filters/tree/master/app/UserSearch) 。 |
| 7 | 6 | |
| 8 | ||
| 7 | ||
| 9 | 8 | #### 我们将创造什么 |
| 10 | 9 | |
| 11 | 10 | 我们公司需要一种跟踪我们与世界各地客户举办的各种活动和会议的方式。我们目前的唯一方法是让每位员工在 Outlook 日程表上存储会议的详细信息。可拓展性较差! | … | … |
| 38 | 37 | |
| 39 | 38 | 前端的条件过滤的截图。 |
| 40 | 39 | |
| 41 | ||
| 40 | ||
| 42 | 41 | #### 模型及模型关联 |
| 43 | 42 | |
| 44 | 43 | 在这个例子中我们回用到很多模型: | … | … |
| 73 | 72 | |
| 74 | 73 | > 客人的名字是 'Billy',来自 'Google' 公司,目前居住在 'London',已经对 'key-note-presentation-new-york-05--2016' 的活动邀请做出了回复,并且回复的内容是 'I will be attending',负责跟进这位客人的销售经理是 'Tom Jones' 或者 'Joe Bloggs'。 |
| 75 | 74 | |
| 76 | ||
| 75 | ||
| 77 | 76 | #### 开始 --- 最佳实践 |
| 78 | 77 | |
| 79 | 78 | 我是一个坚定不移的极简主义者,我坚信少即是多。下面就让我们以最简单的方式探索出解决这个需求的最佳实践。 | … | … |
| 112 | 111 | |
| 113 | 112 | 由于我们需要在 filter 方法中处理请求提交的数据,所以我把 Request 类做了依赖注入。[Laravel 的服务容器](https://laravel.com/docs/5.2/container#resolving) 会解析这个依赖,我们可以在方法中直接使用 Request 的实例,也就是 $request。User 类也是同样道理,我们需要从中检索一些数据。 |
| 114 | 113 | |
| 115 | ||
| 114 | ||
| 116 | 115 | |
| 117 | 116 | 这个搜索需求有一点比较麻烦的是,每个参数都是可选的。所以我们要先写一系列的条件语句来判断每个参数是否存在: |
| 118 | 117 | … | … |
| 150 | 149 | |
| 151 | 150 | 首先,它只会根据一个条件去检索用户表,然后就返回了。所以,通过上面的代码逻辑,我们根本无法获得姓名为 'Billy', 而且住在 'London' 的用户。 |
| 152 | 151 | |
| 153 | ||
| 152 | ||
| 154 | 153 | 实现这种目的的一种方式是嵌套条件: |
| 155 | 154 | |
| 156 | 155 | ``` | … | … |
| 170 | 169 | 我确信你可以看到这在两个或者三个参数的时候起作用,但是一旦我们添加更多选项,这将会难以管理。 |
| 171 | 170 | |
| 172 | 171 | |
| 173 | ||
| 172 | ||
| 174 | 173 | |
| 175 | 174 | |
| 176 | 175 | #### 改进我们的搜索 api | … | … |
| 208 | 207 | |
| 209 | 208 | 好多了!我们现在可以将每个搜索参数做为修饰符添加到从 * $user->newQuery()* 返回的查询实例中。 |
| 210 | 209 | |
| 211 | ||
| 210 | ||
| 212 | 211 | 我们现在可以根据所有的参数来做搜索了, 再多参数都不怕. |
| 213 | 212 | |
| 214 | 213 | 一起来实践吧: | … | … |
| 269 | 268 | 搞定,棒极了! |
| 270 | 269 | |
| 271 | 270 | |
| 272 | ||
| 271 | ||
| 273 | 272 | |
| 274 | 273 | |
| 275 | 274 | #### 是否还需要重构? | … | … |
| 282 | 281 | |
| 283 | 282 | 然而,如果你是在构建一个比较复杂的项目,那么我们还是需要更加优雅且扩展性好的解决方案。 |
| 284 | 283 | |
| 285 | ||
| 284 | ||
| 286 | 285 | |
| 287 | 286 | #### 编写新的搜索 api |
| 288 | 287 | … | … |
| 294 | 293 | |
| 295 | 294 | 根据我的经验,这个方法能帮助我写出可读性更强,更加优雅的程序。还有一个很大的额外收获就是,通过这种阶段性的验收测试,我能更好地抓住商业需求。因此,我可以很自信地说我写的程序可以很好地满足市场的需求,具有很高商业价值。 |
| 296 | 295 | |
| 297 | ||
| 296 | ||
| 298 | 297 | |
| 299 | 298 | 以下添加到搜索功能的代码中,我希望我的搜索 api 是这样写的: |
| 300 | 299 | … | … |
| 323 | 322 | } |
| 324 | 323 | ``` |
| 325 | 324 | |
| 326 | ||
| 325 | ||
| 327 | 326 | |
| 328 | 327 | 最简单的方式,让我们把控制器中的代码复制到 apply 函数中: |
| 329 | 328 | … | … |
| 406 | 405 | |
| 407 | 406 | |
| 408 | 407 | |
| 409 | ||
| 408 | ||
| 410 | 409 | |
| 411 | 410 | 其次,由于 `newQuery()` 方法不是静态方法,无法通过 User 类静态调用,所以我们需要先创建一个 User 对象,再调用这个方法: |
| 412 | 411 | … | … |
| 437 | 436 | |
| 438 | 437 | 好多了,是不是?把一系列的条件判断交给专门的类处理,使控制器的代码简介清新。 |
| 439 | 438 | |
| 440 | ||
| 439 | ||
| 441 | 440 | |
| 442 | 441 | #### 下面进入见证奇迹的时刻 |
| 443 | 442 | … | … |
| 451 | 450 | |
| 452 | 451 | 先从 `Name` 条件开始吧。但是,就像我们前面讲的,还是想一下我们需要怎样使用这种单一条件过滤的接口。 |
| 453 | 452 | |
| 454 | ||
| 453 | ||
| 455 | 454 | 我希望可以这样调用这个接口: |
| 456 | 455 | |
| 457 | 456 | ``` | … | … |
| 511 | 510 | |
| 512 | 511 | 如你所见,City 类的代码逻辑跟 Name 类相同,只是过滤条件变成了 'city'。让每个条件过滤类都只有一个简单的 `apply()` 方法,而且方法接收的参数和处理的逻辑都相同,我们可以把这看成一个协议,这一点很重要,下面我会具体说明。 |
| 513 | 512 | |
| 514 | ||
| 513 | ||
| 515 | 514 | 为了确保每个条件过滤类都能遵循这个协议,我决定写一个接口,让每个类都实现这个接口。 |
| 516 | 515 | |
| 517 | 516 | ``` | … | … |
| 596 | 595 | |
| 597 | 596 | 我把所有的条件过滤类的文件放在一个单独的文件夹里,这让我对已有的过滤条件一目了然。 |
| 598 | 597 | |
| 599 | ||
| 598 | ||
| 600 | 599 | #### 使用新的过滤器 |
| 601 | 600 | |
| 602 | 601 | 现在回过头来看 UserSearch 类的 applyFiltersToQuery() 方法,发现我们可以再做一些优化了。 | … | … |
| 615 | 614 | } |
| 616 | 615 | ``` |
| 617 | 616 | |
| 618 | ||
| 617 | ||
| 619 | 618 | |
| 620 | 619 | 现在根据过滤条件构建查询语句的工作已经转给各个相应的过滤类了,但是判断每个过滤条件是否存在的工作,还是通过一系列的条件判断语句完成的。而且条件判断的参数都是写死的,一个参数对应一个过滤类。这样我每增加一个新的过滤条件,我都要重新修改 `UserSearch` 类的代码。这显然是一个需要解决的问题。 |
| 621 | 620 | … | … |
| 649 | 648 | } |
| 650 | 649 | ``` |
| 651 | 650 | |
| 652 | ||
| 651 | ||
| 653 | 652 | |
| 654 | 653 | 下面逐行分析这段代码: |
| 655 | 654 | … | … |
| 687 | 686 | $query = $decorator::apply($query, $value); |
| 688 | 687 | ``` |
| 689 | 688 | |
| 690 | 这里就是神 | |
| 691 |
| |
| 692 |
| |
| 693 | ||
| 689 | 这里就是神奇的地方了,PHP 允许把变量 `$decorator` 作为类,并调用其方法(在这里就是 apply() 方法了)。现在再看这个接口的代码,发现我们再次实力证明了磨刀不误砍柴工。现在我们可以确保每个过滤类对外响应一致,内部又可以分别处理各自的逻辑。 | |
| 690 | ||
| 691 | ||
| 692 | ||
| 694 | 693 | |
| 695 | 694 | |
| 696 | 695 | #### 最后的优化 | … | … |
| 758 | 757 | |
| 759 | 758 | 这样,即便要扩展搜索接口,我也不需要再去反复修改 `UserSearch` 类里的代码了。需要增加新的过滤条件吗?简单,只要在 `App\UserSearch\Filters` 目录下创建一个过滤类,并使之实现 `Filter` 接口就 OK 了。 |
| 760 | 759 | |
| 761 | ||
| 760 | ||
| 762 | 761 | |
| 763 | 762 | #### 结论 |
| 764 | 763 |
关于 LearnKu