Whip Monstrous Code Into Shape - 03 Consider Domain Events
If it makes sense for your application, you might consider leveraging domain events and listeners to construct a complex set of functionality.
事件机制是一种很好的应用解耦方式,因为一个事件可以拥有多个互不依赖的监听器。举例来说,每次你把用户的订单发完货后都会希望给他发个 Slack 通知。这时候你就可以发起一个 OrderShipped 事件,它会被对应的监听器接收到再传递给 Slack 通知模块,这样你就不用把订单处理的代码跟 Slack 通知的代码耦合在一起了。
其实在文档中已经写的很明白,但是不知道大家是不是这样,至少我是,被文档中的例子所束缚,想的总是用来做一些发邮件、 HTTP 请求等比较耗时的工作,但是其实这是队列的作用,而事件是用来解耦的。
刚开始用事件的时候总是很奇怪,为什么要用事件,这跟写在控制器中根本就没什么区别嘛。等到后来控制器越来越臃肿的时候,才慢慢关注代码如何解耦、如何更优雅。
事件估计大家用的比较多,就不举例了,无非就是一个事件可以对应多个监听器,而这些监听器可以互相没有任何关系,达到解耦的目的。
获取事件的返回值
这个视频比较简单,我写完之后发现好像太没意思了,于是我就看了看源码,看能不能找到一些有趣的东西,还真的看到一个平常不太了解的:触发事件可以有返回值,这个在文档上好像没有看到,并且平时使用的时候也不太会这样用。
先看看 event 函数,很简单,就是调用 Illuminate\Events\Dispatcher::fire($event, $payload = [], $halt = false)
,事件看起来复杂,其实就是一个 Dispatcher,所有的绑定、fire等都是这个在处理。
function event(...$args)
{
return app('events')->fire(...$args);
}
再看看 fire 方法,有三个参数:第一个是事件,可以是事件名也可以是一个事件实例;第二个参数是 payload(载荷?),也就是在 Listener handle方法要用到的参数,不仅仅是事件实例本身,如果有额外参数,可以用数组形式传进去;第三个参数默认是 false,设置为真时当 Listener 有返回值时就会中断事件的传递。
public function fire($event, $payload = [], $halt = false)
{
// 如果 $event 是事件实例的话,使用该实例类名作为事件名,
// 并使用该实例作为 listener handler 方法的参数
if (is_object($event)) {
list($payload, $event) = [[$event], get_class($event)];
}
// 这就是返回值数组...
$responses = [];
// 参数默认是空数组,如果传入了值的话也会转换成数组,方便使用 call_user_func_array 函数
if (! is_array($payload)) {
$payload = [$payload];
}
$this->firing[] = $event;
// 是否要广播
if (isset($payload[0]) && $payload[0] instanceof ShouldBroadcast) {
$this->broadcastEvent($payload[0]);
}
foreach ($this->getListeners($event) as $listener) {
$response = call_user_func_array($listener, $payload);
// 如果 listener 有返回值并且 $halt 设置为 true 的话,
// 直接返回结果,并且中断事件继续传播
if (! is_null($response) && $halt) {
array_pop($this->firing);
return $response;
}
// 如果 listener 返回 false 的话中断事件继续传播
if ($response === false) {
break;
}
$responses[] = $response;
}
array_pop($this->firing);
return $halt ? null : $responses;
}
所以你可以获取 Listener 中返回的值数组:$responses = event(new UserRegistered($user));
修改事件传播时的 payload
在用事件的时候我在想一个问题,怎么在事件的传播中动态的修改事件的载荷,也就是 Listener handle 方法里面的参数,比如 UserRegistered($user)
事件来说,第一个监听器会修改 $user
,然后第二个 Listener 的 handle 方法参数也是UserRegistered($user)
,修改后的$user 不会对第二个 Listener 起作用。
后来想想太简单了,因为 UserRegistered($user)
本身就是一个数据容器,$user
也是 public ,直接在第一个监听器处理完成后修改事件就行了 :$event->user = $user
,因为在事件 fire 的时候不管多少个监听器,用的都是用一个事件实例。
这个也给我一个启发,虽然事件仅仅是一个数据容器,但是我们也可以在里面做一些数据的修改供 Listener 使用,说不定在有些情况下有奇效。
乱七八糟的东西
最后在这个视频中有一点我觉得记录一下,挺有意思的。
我们可以适当的把一些简单的逻辑放到模型中,当然太复杂的也不行,这样造成模型臃肿,如何选择一个最适合的方式,这应该是写出优雅代码必须要考虑的。
比如用户注册并触发事件,可以写在控制器中:
$user = User::create([
'name' => 'example'
'email' => 'test@example.com',
'password' => bcrypt('123456'),
]);
event(new UserRegistered($user));
同样的,可以把这部分封装到模型中,在控制器中 User::register(...)
就行:
public static function register($attributes)
{
$user = User::create($attributes);
event(new UserRegistered($user));
}
这样做法会带来一个问题,增加理解难度,可能过个十天半月你去看控制器,哎,为什么控制器里面只有一个简单的 User::register(...)
方法?当然可以去查看 User::register 方法,同样的你也可以写个注释,就像这样:
/**
* @event App\Event\UserRegistered
*/
public function store()
{
return User::register();
}
推荐文章: