# 事件机制 ## 前言 事件模式必须基于 [PSR-14](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-14-event-dispatcher.md) 去实现。 Hyperf 的事件管理器默认由 [hyperf/event](https://github.com/hyperf/event) 实现,该组件亦可用于其它框架或应用,只需通过 Composer 将该组件引入即可。 ```bash composer require hyperf/event ``` ## 概念 事件模式是一种经过了充分测试的可靠机制,是一种非常适用于解耦的机制,分别存在以下 3 种角色: - `事件(Event)` 是传递于应用代码与 `监听器(Listener)` 之间的通讯对象 - `监听器(Listener)` 是用于监听 `事件(Event)` 的发生的监听对象 - `事件调度器(EventDispatcher)` 是用于触发 `事件(Event)` 和管理 `监听器(Listener)` 与 `事件(Event)` 之间的关系的管理者对象 用通俗易懂的例子来说明就是,假设我们存在一个 `UserService::register()` 方法用于注册一个账号,在账号注册成功后我们可以通过事件调度器触发 `UserRegistered` 事件,由监听器监听该事件的发生,在触发时进行某些操作,比如发送用户注册成功短信,在业务发展的同时我们可能会希望在用户注册成功之后做更多的事情,比如发送用户注册成功的邮件等待,此时我们就可以通过再增加一个监听器监听 `UserRegistered` 事件即可,无需在 `UserService::register()` 方法内部增加与之无关的代码。 ## 使用事件管理器 > 接下来我们会通过配置和注解两种方式介绍监听器,实际使用时,二者只需使用其一即可,如果既有注解又有配置,则会造成监听器被多次触发。 ### 定义一个事件 一个事件其实就是一个用于管理状态数据的普通类,触发时将应用数据传递到事件里,然后监听器对事件类进行操作,一个事件可被多个监听器监听。 ```php user = $user; } } ``` ### 定义一个监听器 监听器都需要实现一下 `Hyperf\Event\Contract\ListenerInterface` 接口的约束方法,示例如下。 ```php user; } } ``` #### 通过配置文件注册监听器 在定义完监听器之后,我们需要让其能被 `事件调度器(Dispatcher)` 发现,可以在 `config/autoload/listeners.php` 配置文件 *(如不存在可自行创建)* 内添加该监听器即可,监听器的触发顺序根据该配置文件的配置顺序: ```php user; } } ``` 在通过注解注册监听器时,我们可以通过设置 `priority` 属性定义当前监听器的顺序,如 `@Listener(priority=1)` ,底层使用 `SplPriorityQueue` 结构储存,`priority` 数字越大优先级越高。 > 使用 `@Listener` 注解时需 `use Hyperf\Event\Annotation\Listener;` 命名空间; ### 触发事件 事件需要通过 `事件调度器(EventDispatcher)` 调度才能让 `监听器(Listener)` 监听到,我们通过一段代码来演示如何触发事件: ```php save(); // 完成账号注册的逻辑 // 这里 dispatch(object $event) 会逐个运行监听该事件的监听器 $this->eventDispatcher->dispatch(new UserRegistered($user)); return $result; } } ``` ## Hyperf 生命周期事件 ![](imgs/hyperf-events.svg) ## 注意事项 ### 不要在 `Listener` 中注入 `EventDispatcherInterface` 因为 `EventDispatcherInterface` 依赖于 `ListenerProviderInterface`,而 `ListenerProviderInterface` 初始化的同时,会收集所有的 `Listener`。 而如果 `Listener` 又依赖了 `EventDispatcherInterface`,就会导致循坏依赖,进而导致内存溢出。 ### 最好只在 `Listener` 中注入 `ContainerInterface`。 最好只在 `Listener` 中注入 `ContainerInterface`,而其他的组件在 `process` 中通过 `container` 获取。框架启动开始时,会实例化 `EventDispatcherInterface`,这个时候还不是协程环境,如果 `Listener` 中注入了可能会触发协程切换的类,就会导致框架启动失败。 ### `BootApplication` 事件尽量避免 IO 操作 > `1.1.6` 版本及更新的版本已优化此问题 在 `1.1.6` 版本之前,因为 `BootApplication` 是在 `Command` 初始化 和 `Server` 启动前触发,所以当前环境一定是非协程环境,一旦使用了协程 `API`,则会导致启动失败。