从零开始编写一个PHP框架 系列的《事件管理模块》
项目地址:terse
前言
事件管理器在框架中早已屡见不鲜,也有不少人称之为“钩子”,它在组件和需要侦听类之间充当着接口角色,这里我们将要实现一个事件管理器。
需求分析
- 绑定监听事件
- 事件触发
- 移除监听事件
- 事件冒泡
- 事件优先级
目录结构
既然是事件管理器,那肯定要分为多个部分,事件和管理。
.
├── Event.php [事件类]
└── Manager.php [管理类]
事件类
先看一段 jQuery
的事件绑定代码。
var deleteMessage = 'delete current row';
$('.button').on('click', '.delete', function(e){
e.stopPropagation();
console.log(deleteMessage)
});
从上面的代码,我们分析到几点:
1、我们需要知道监听的是什么事件,比如:click
2、我们需要知道监听的对象是什么,比如:.delete
3、我们需要知道监听事件后做什么,比如:function(e){...}
4、我们需要知道触发时需要的参数,比如:deleteMessage
5、我们需要知道是否需要冒泡触发,比如:e.stopPropagation()
分析完成,我们就可以确定事件类的基本属性和方法了。
class Event
{
/**
* 监听事件名称
*
* @var [type]
*/
protected $_name;
/**
* 监听的对象
*
* @var [type]
*/
protected $_source;
/**
* 参数
*
* @var [type]
*/
protected $_data;
/**
* 是否停止冒泡
*
* @var boolean
*/
protected $_stopped = false;
/**
* 构造函数
*
* @param string $name
* @param mixed $source
* @param mixed $data
* @param boolean $stopped
*/
function __construct(string $name, $source, $data = null, $stopped = false)
{
$this->_name = $name;
$this->_source = $source;
$this->_data = $data;
$this->_stopped = (bool)$stopped;
}
/**
* 设置监听事件的名称
*
* @param string $name
*/
public function setName(string $name) {
$this->_name = $name;
return $this;
}
/**
* 获取监听事件的名称
*
* @return string
*/
public function getName() {
return $this->_name;
}
/**
* 设置监听的对象
*
* @param mixed $source
*/
public function setSource($source) {
$this->_source = $source;
return $this;
}
/**
* 获取监听的对象
*
* @return mixed
*/
public function getSource() {
return $this->_source;
}
/**
* 设置参数
*
* @param mixed $data
*/
public function setData($data) {
$this->_data = $data;
return $this;
}
/**
* 获取参数
*
* @return mixed
*/
public function getData() {
return $this->_data;
}
/**
* 停止冒泡
*
* @return void
*/
public function stop() {
$this->_stopped = true;
}
/**
* 是否停止冒泡
*
* @return boolean
*/
public function isStop() {
return $this->_stopped;
}
}
细心的小伙伴应该看出了一个问题,就是我们总结的时候有五点,然而实现的时候只实现了四点。这是为什么呢?
因为我们在绑定事件的时候,会加上触发后要做的事情。然而在触发的时候,我们只需要知道触发什么事件,对谁触发,需要的参数等。
所以,事件触发后要做的事情会放在管理器里保存,也就是在绑定事件的时候指定。
管理类
如我们一开始的需求分析,我们需要提供几个方法。
绑定监听事件 ---> attach()
移除监听事件 ---> detach()
触发事件 ---> trigger()
清空事件 ---> empty()
绑定监听事件
按照 事件类
最后的说明,我们在绑定事件的时候,需要指定三个参数,分别是:绑定类型
、触发后做的事情
、优先级
。
过程分析:
- 事件如果不存在,需要初始化事件
- 如果存在,则找到第一个优先级比自己低的事件(倒序排序)
如果找到,则将当前事件插入到这个事件之前
如果没有找到,则将当前事件插入到事件列表末尾
<?php
class Manager
{
/**
* 事件
*
* @var array
*/
protected $_events = [];
/**
* 绑定监听事件
*
* @param string $name
* @param mixed $handler
* @param integer $priority
* @return void
*/
public function attach(string $name, $handler, $priority = 100)
{
$events = $this->_events;
$data = [
'handler' => $handler,
'priority' => $priority
];
// 如果事件不存在,初始化赋值
if (!isset($events[$name])) {
$events[$name][] = $data;
$this->_events = $events;
return true;
}
// 需要插入的位置
$insertKey = null;
// 找到第一个优先级比自己低的事件的key
foreach ($events[$name] as $key => $event) {
if ($event['priority'] < $priority) {
$insertKey = $key;
break;
}
}
// 如果没有找到,说明自己的优先级最低
if (is_null($insertKey)) {
$events[$name][] = $data;
} else {
// 找到则插入事件
array_splice($events[$name], $insertKey, 0, [$data]);
}
$this->_events = $events;
}
...
}
移除监听事件
移除事件有两种情况:
1、移除某个事件列表中的某一个事件
2、移除某个事件列表
基于以上两点,我们需要两个参数,事件列表名称和事件触发后的操作。
过程分析:
- 没找到指定类型的事件列表,返回真,表明已经移除
- 找到指定类型的事件列表
没有指定需要移除的事件,直接移除此事件列表,返回真
有指定需要移除的事件,找到该事件并移除(如果相同类型的事件列表中有多个相同的事件,这里只移除其中一个)
关于移除指定事件列表的指定事件上,有点问题😂,一般不建议使用
<?php
class Manager
{
...
/**
* 移除监听事件
*
* @param string $name
* @param mixed $handler
* @return true
*/
public function detach(string $name, $handler = null)
{
$events = $this->_events;
// 不存在则返回真,表明移除成功
if (!isset($events[$name])) {
return true;
}
// 如果不是移除事件列表中的某一个,而是移除某一类,则移除后返回真
if (!$handler) {
unset($this->_events[$name]);
return true;
}
// 找到事件,并移除
// 如果出现同一类事件列表中出现多个相同事件,表明逻辑本身有问题,这里不加考虑
foreach ($events[$name] as $key => $event) {
if ($event['handler'] == $handler) {
unset($this->_events[$name][$key]);
break;
}
}
return true;
}
}
清空监听事件
这是最简单的了。
<?php
class Manager
{
...
/**
* 清空监听事件
*
* @return void
*/
public function clear()
{
$this->_events = [];
}
}
触发监听事件
麻烦来了...
触发监听事件,我们需要知道几点:
1、触发的是哪个事件(必须)
2、触发的事件需要什么参数(可选)
3、可能是针对某个类进行的触发,那么这个类是什么(可选,优先级最低)
过程分析:
1、事件列表是否存在,不存在则返回真
2、初始化事件信息
3、执行该列表下的每一个事件
4、每执行完一个事件,检测一下是否需要停止,如果停止,则返回
5、可能会出现执行 db.beforeQuery
的情况,那么 db.beforeQuery
事件列表执行完之后,执行 db
事件列表(人工冒泡)。
<?php
class Manager
{
...
/**
* 触发监听事件
*
* @return [type] [description]
*/
/**
* 触发监听事件
*
* @param string $name
* @param mixed $source
* @param array $data
* @return boolean
*/
public function trigger(string $name, array $data = [], $source = null)
{
$events = $this->_events;
if (!isset($events[$name])) {
return false;
}
$result = false;
// 初始化事件
$event = new Event($name, $source, $data);
// 先执行当前绑定事件
$result = $this->triggerEvents($events[$name], $event);
if ($event->isStop()) {
return $result;
}
// 再寻找上一级
if (strpos($name, '.') < 1) {
return $result;
}
// 拆分模块
list($type, $eventName) = explode('.', $name);
if (!isset($events[$type])) {
return $result;
}
// 赋值新的模块
$event->setName($eventName);
// 触发事件
return $this->triggerEvents($events[$type], $event);
}
}
在触发事件的时候,我们将触发事件的过程给剥离出来了。
过程分析:
1、检测 handler
是否是对象,如果不是对象,则不执行
2、如果是函数,直接调用执行
3、如果是类,则判断 name
是否是类的某一个方法,如果是,则执行
<?php
class Manager
{
...
/**
* 触发监听事件
*
* @param array $events
* @param Event $eventObj
* @return true
*/
protected function triggerEvents(array $events, Event $eventObj)
{
foreach ($events as $event) {
$handler = $event['handler'];
// 不是对象那还弄什么
if (!is_object($handler)) {
continue;
}
// 如果是函数,则直接执行
if ($handler instanceof \Closure) {
call_user_func_array($handler, [$eventObj]);
if ($eventObj->isStop()) {
break;
}
continue;
}
// 不是函数就是类了
$method = $eventObj->getName();
if (method_exists($handler, $method)) {
call_user_func_array([$handler, $method], [$eventObj]);
if ($eventObj->isStop()) {
break;
}
}
}
return true;
}
}
到此为止,事件管理模块就告一段落了。
完整代码
事件定义类:
<?php
namespace Terse\Event;
/**
* Terse\Event\Event
*
* @link https://gitee.com/imjcw/terse
* @author imjcw <imjcw@imjcw.com>
*/
class Event
{
/**
* 监听事件名称
*
* @var [type]
*/
protected $_name;
/**
* 监听的对象
*
* @var [type]
*/
protected $_source;
/**
* 参数
*
* @var [type]
*/
protected $_data;
/**
* 是否停止冒泡
*
* @var boolean
*/
protected $_stopped = false;
/**
* 构造函数
*
* @param string $name
* @param mixed $source
* @param mixed $data
* @param boolean $stopped
*/
function __construct(string $name, $source, $data = null, $stopped = false)
{
$this->_name = $name;
$this->_source = $source;
$this->_data = $data;
$this->_stopped = (bool)$stopped;
}
/**
* 设置监听事件的名称
*
* @param string $name
*/
public function setName(string $name) {
$this->_name = $name;
return $this;
}
/**
* 获取监听事件的名称
*
* @return string
*/
public function getName() {
return $this->_name;
}
/**
* 设置监听的对象
*
* @param mixed $source
*/
public function setSource($source) {
$this->_source = $source;
return $this;
}
/**
* 获取监听的对象
*
* @return mixed
*/
public function getSource() {
return $this->_source;
}
/**
* 设置参数
*
* @param mixed $data
*/
public function setData($data) {
$this->_data = $data;
return $this;
}
/**
* 获取参数
*
* @return mixed
*/
public function getData() {
return $this->_data;
}
/**
* 停止冒泡
*
* @return void
*/
public function stop() {
$this->_stopped = true;
}
/**
* 是否停止冒泡
*
* @return boolean
*/
public function isStop() {
return $this->_stopped;
}
}
事件管理类:
<?php
namespace Terse\Event;
// 绑定单个监听事件 ---> attach()
// 批量绑定监听事件 ---> batchAttach()
// 触发事件 ---> trigger()
// 移除单个监听事件 ---> detach()
// 批量移除监听事件 ---> batchDetach()
// 清空事件 ---> empty()
/**
* Terse\Event\Manager
*
* @link https://gitee.com/imjcw/terse
* @author imjcw <imjcw@imjcw.com>
*/
class Manager
{
/**
* 事件
*
* @var array
*/
protected $_events = [];
public function getEvents()
{
return $this->_events;
}
/**
* 绑定监听事件
*
* @param string $name
* @param mixed $handler
* @param integer $priority
* @return void
*/
public function attach(string $name, $handler, $priority = 100)
{
$events = $this->_events;
$data = [
'handler' => $handler,
'priority' => $priority
];
// 如果事件不存在,初始化赋值
if (!isset($events[$name])) {
$events[$name][] = $data;
$this->_events = $events;
return true;
}
// 需要插入的位置
$insertKey = null;
// 找到第一个优先级比自己低的事件的key
foreach ($events[$name] as $key => $event) {
if ($event['priority'] < $priority) {
$insertKey = $key;
break;
}
}
// 如果没有找到,说明自己的优先级最低
if (is_null($insertKey)) {
$events[$name][] = $data;
} else {
// 找到则插入事件
array_splice($events[$name], $insertKey, 0, [$data]);
}
$this->_events = $events;
}
/**
* 移除监听事件
*
* @param string $name
* @param mixed $handler
* @return true
*/
public function detach(string $name, $handler = null)
{
$events = $this->_events;
// 不存在则返回真,表明移除成功
if (!isset($events[$name])) {
return true;
}
// 如果不是移除事件列表中的某一个,而是移除某一类,则移除后返回真
if (!$handler) {
unset($this->_events[$name]);
return true;
}
// 找到事件,并移除
// 如果出现同一类事件列表中出现多个相同事件,表明逻辑本身有问题,这里不加考虑
foreach ($events[$name] as $key => $event) {
if ($event['handler'] == $handler) {
unset($this->_events[$name][$key]);
break;
}
}
return true;
}
/**
* 清空监听事件
*
* @return void
*/
public function clear()
{
$this->_events = [];
}
/**
* 触发监听事件
*
* @return [type] [description]
*/
/**
* 触发监听事件
*
* @param string $name
* @param mixed $source
* @param array $data
* @return boolean
*/
public function trigger(string $name, array $data = [], $source = null)
{
$events = $this->_events;
if (!isset($events[$name])) {
return false;
}
$result = false;
// 初始化事件
$event = new Event($name, $source, $data);
// 先执行当前绑定事件
$result = $this->triggerEvents($events[$name], $event);
if ($event->isStop()) {
return $result;
}
// 再寻找上一级
if (strpos($name, '.') < 1) {
return $result;
}
// 拆分模块
list($type, $eventName) = explode('.', $name);
if (!isset($events[$type])) {
return $result;
}
// 赋值新的模块
$event->setName($eventName);
// 触发事件
return $this->triggerEvents($events[$type], $event);
}
/**
* 触发监听事件
*
* @param array $events
* @param Event $eventObj
* @return true
*/
protected function triggerEvents(array $events, Event $eventObj)
{
foreach ($events as $event) {
$handler = $event['handler'];
// 不是对象那还弄什么
if (!is_object($handler)) {
continue;
}
// 如果是函数,则直接执行
if ($handler instanceof \Closure) {
call_user_func_array($handler, [$eventObj]);
if ($eventObj->isStop()) {
break;
}
continue;
}
// 不是函数就是类了
$method = $eventObj->getName();
if (method_exists($handler, $method)) {
call_user_func_array([$handler, $method], [$eventObj]);
if ($eventObj->isStop()) {
break;
}
}
}
return true;
}
}
最后
解除绑定事件慎用,如果使用不得当,会造成解除失败的情况,而系统并不会报错。
下一篇《请求模块》