跟控制器说再见吧,从今天开始使用请求处理器(Request Handlers) 范式
在过去几年中, PHP 开发环境发生了很大的变化。我们开始使用更多更好的设计模式,比如 DRY 和 SOLID 设计模式原则。但为什么我们仍然在使用控制器?
如果您以前曾经参与过大型项目的架构编写,那么您可能已经注意到迟早会出现控制器过多的这种现象。即使您将控制器逻辑分离到各种类库或服务类中,大量的依赖项和方法以及代码的行数还是会随着时间的推移不断增长。
我来介绍一下请求处理器。这个概念很简单,但很多 PHP 开发人员都不知道。请求处理器可以理解为仅包含单个动作(Action
)的控制器,能够使请求到响应的流程更加清晰明确。这个概念与 Paul M. Jones 提出的 Action-Domain-Responder 设计模式有相似之处,后者是MVC模式的替代品。
一个好的方法去建立请求处理器就是使用调用类。可调用类是使用PHP中的魔术方法 __invoke
,把他们变成一个 Callable ,这将允许他们作为函数调用。这里有一个关于调用类的简单例子:
class Greeting
{
public function __invoke($name)
{
echo 'Hello ' . $name;
}
}
$welcome = new Greeting();
$welcome('John Doe'); //输出 Hello John Doe
看到这里你大概会想;“我为什么要这样做?”。我知道这是一个有点荒谬的例子。但是它与某些代码一起使用时例如可调用对象和依赖注入,它将变得很有意义。Laravel 和 Slim 路由的请求处理就是一个非常好的例子。
Route::get('/{name}', Greeting::class);
是否让你大吃一惊?没有?让我们把它和你通常写的比较一下:
Route::get('/{name}', 'SomeController@greeting');
还没有?除了代码好看之外,还有其他优点。让我们先去看看使用请求处理程序比控制器有哪些优点。
单一模式
SOLID 的第一个原则是“单一模式”。在我看来,控制器中存在许多的方法,就打破了这个原则。请求处理程序提供了一个很好的解决方案,可以将这些操作分成它们自己的类,使它们更易于维护,重构和测试。
这是从 UsersController
中提取的2个请求处理程序的示例,它处理用户配置文件的编辑和保存:
class EditUserHandler
{
public function __construct(
UserRepository $repository,
Twig $twig
) {
...
}
public function __invoke(Request $request, Response $response)
{
...
}
}
class UpdateUserHandler
{
public function __construct(
UserRepository $repository,
UpdateUserValidator $validator,
ImageManager $resizer,
Filesystem $storage
) {
...
}
public function __invoke(Request $request, Response $response)
{
...
}
}
接下来让我们看下一个优势;
测试性能
你最近有没有为你的项目编写过单元测试?在编写单元测试的时候你可能编写了一些与测试无关的模拟依赖项。由于请求处理器将不同的控制器操作拆分为单独的类,因此您只需注入或绑定该动作所需要的依赖项即可。
这是 Jeffrey Way 的一些建议 Twitter 。
提示:让你的功能测试尽可能更加详细具体,使用测试用例来描述重要的规则和能力。
这基本不会让你的请求处理器都有一个测试文件。对于那些繁琐的控制器测试文件来说是一个非常好的改进。
重构
PhpStorm 和其他的编辑器都有强大的代码重构功能,但是如果你使用的是 Laravel 或者 Slim 框架默认的路由方法将控制器绑定到路由,那么你可能会遇到这种问题。
例如重命名:
Route::get('/{name}', Greeting::class);
比这简单得很多:
Route::get('/{name}', 'SomeController@greeting');
结论
请求处理器是控制器很好的替代品。控制器的动作(Actions
)被分为多个独立的请求处理器类,分别负责响应单一的动作。这使整个项目的代码更易于维护、重构和测试。
您是否应当使用请求处理器替换所有控制器?可能不是。对于小型应用程序而言,为了简单,将动作组合成控制器或许更加合理。当我开始在 Teamleader 工作后,我才开始发掘请求处理器,我觉得近期没什么换回控制器的必要了。
如果有什么不清楚或有疑问,请在下面留下评论告诉我,我会更新这篇文章。
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
高认可度评论:
虽然我觉得
'SomeController@greeting'
不好,但是Greeting::class
也不是特别优雅从业务角度来说没有简单的业务,就拿更新来说,一个更新会牵涉到权限验证,表单数据验证,日志记录,DB存储,事件分发等一系列的操作,你这里解决的只是从入口上看上去是有一个类来完成的这些操作,我个人的经验是使用函数式编程的方法,在controller里面使用链式调用,这样从逻辑上更贴合自然思维。
以上纯属个人观点,没别的意思,楼主的这个思想有点KOA的意思,本身也是很有意思的解决思路。但是对于不同的项目规模,业务复杂度和团队能力我们需要选择不同的实现思路。会思考才是我们最大的优势。
一个请求,一个接口,一个处理的类文件,一个测试文件。用了 GraphQL 之后,爱上这种方式,也不想回去了。
这个姿势有点 Sao
虽然我觉得
'SomeController@greeting'
不好,但是Greeting::class
也不是特别优雅:smile: 可以 很嗨 以单文件 多个动作类 来替代单文件 单类 多方法 确实不错 __invoke这个魔术方法 之前在看slim框架的 中间件的时候 仔细看过 活学活用 :+1:
从业务角度来说没有简单的业务,就拿更新来说,一个更新会牵涉到权限验证,表单数据验证,日志记录,DB存储,事件分发等一系列的操作,你这里解决的只是从入口上看上去是有一个类来完成的这些操作,我个人的经验是使用函数式编程的方法,在controller里面使用链式调用,这样从逻辑上更贴合自然思维。
以上纯属个人观点,没别的意思,楼主的这个思想有点KOA的意思,本身也是很有意思的解决思路。但是对于不同的项目规模,业务复杂度和团队能力我们需要选择不同的实现思路。会思考才是我们最大的优势。
Rust 的 actix web 框架也是基于这个思路
一个请求,一个接口,一个处理的类文件,一个测试文件。用了 GraphQL 之后,爱上这种方式,也不想回去了。
我们之前项目负责人就是嫌service层肥大,而且并不公用,就要求一个接口一个控制器,在控制器中有一个index方法,其它的方法都服务这个index方法,我觉得这也是一个很好的思路。跟文中的思路其实很像。
@zhuzhichao 测试 GraphQL 的测试文件一般怎么写呢
用这种方法,Laravel最基本的依赖注入就已经没法用了吧?
@笛轻 你用的是哪个 GraphQL 的包? 对应的项目的 github 上面都会有 tests 文件夹,你进去看看,参考一下他们怎么写的,比葫芦画瓢就行。
一个请求,一个接口,一个处理的类文件,一个测试文件,会不会太麻烦了
要写很多文件,很麻烦,效率低,但是利于维护和解耦,看怎么平衡
apiato 应该也是这个思路,不过很长时间不更新了
现在artisan创建控制器时可以直接加上--invokable就可以创建这样的控制器了
这种模式写复杂逻辑项目非常 nice
@Koba
了解下
__invoke
魔术方法就知道了。刚刚在我现有的刚开始的项目重构时,遇到些问题
在书写api时返回的是一个json格式的信息,没有办法单一功能重复多次使用
由于我已经用了services层来注入控制器,现在我还是恢复了以下模式:
控制器:传参+返回json
服务类:处理单一功能,返回数据