原文: https://blog.csdn.net/yhl27/article/details/8705313
--------------------------------------------------------------------------
php中实现事件驱动
php 事件驱动编程:(http://hi.baidu.com/yiqing95)
*
事件驱动在桌面型应用中是非常普遍的,比如你点击鼠标,点击某个按钮应用程序就得对你的动作做出相应的反应,从程序员的角度看,有两个角色需要识别:一个是用户 你个就是你构建的系统,此外无它! 现在用中间者的身份看二者的行为模式:用户总是在向系统发送某种信息,这种信息促使系统进行响应,所有的请求从更抽象的角度观察,无外乎两种:命令与请求;请严格区分这两者,命令是促使系统做某种变更(比如删除一条记录),而请求是需要系统返回某些信息(比如查询);当然命令也一般会反馈一些信息告诉系统执行情况(是成功了还是失败了,还是发生了不可预知的异常。);从这里可以得到一个适应所有情况的编程接口
execute(IN paramInformation):returnedInformation; //参数和返回值有时可选
事件模型是对 “变更——响应”机制的实现,一种机制总是可用多个方法来实现,这取决于编程者如何想问题的。php内部并没有所谓的事件,事件分发器的概念,但并不代表我们不能用事件模型来处理php世界里的问题!!
*
**
理解事件:
什么是事件,从你易接受的角度看就是键盘,鼠标等人物动作引起的变更,这个变更就叫事件:鼠标单击事件,鼠标拖动事件,键盘点击事件...... 。 从我们人类对世界的认识角度来看:所有对你,或对大家有意义的变更就是事件,比如918事件,1212事件911事件,自然界无时不刻发生着某种变化,而只有这些变化引起你足够重视时他才是事件,比如你可以说1980年发生了一个重大事件---那一天我诞生了(这对你父母来说当然是重大事件,至少比911重大些);所以最后阐明一点:事件就是变化,任何变化的结果都能被定义为事件,所以什么时事件有你来决定。
从计算机角度看,所有的01序列的位变化都可定义为事件,仍取决于你,但计算机最底层的实现无外乎读与写,所以读和写性质的操作结果(或者这个发生)就可以被称为事件。还有一个比较特殊的变化是时间,从四维空间来看虽然三维空间上的某些东西没有变,但时间维总是在向前推进,所有时间也可以引起事件。
任何变更或条件的满足都能被定义为事件,如果不产生信息交换,一个封闭的系统对外界不会产生有意义的事件,你如果只设计了一个没有键盘和鼠标的电脑,估计电脑充其量也就是一个看着复杂的电视或DVD而已。为此程序都向外部暴露了某些接口,通过这些接口我们就能够触发某些变更。这些接口可以是GUI上的可视化接口,也可以是命令行方式的接口,当然从仿形递归角度看,更小一些的组件(子系统,类..)会向外界暴露一些API。
外界暴露的接口是通向系统内部的引脚,也是触发系统内部事件的导火索,在web开发中这些接口,如你所见比如超链接,按钮,输入框,下拉菜单等,这些东西都能引发系统对其响应。从上面的事件理解来看,事件是可以被忽略的(比如911对别人来说是事件对我就不是,又不会影响我什么),而且要紧的一点是事件可以传播!!变化是可以导致变化的,所以一个事件就有可能导致另一个事件。这些都是事件的特征。值得一提的事想想如果形成环行事件传播路径会怎样??
**
***
我们是系统的设计者,从系统角度来看我们需要响应用户对系统的请求与命令。也就是需要响应一些事件,然后做某些处理,把最终的执行情况或请求的信息返回给客户。在系统的边界处我们要把这些事件跟系统内部预定义好的响应模块映射起来:
switch($_GET[‘action’]) {
case “edit_record”:
edit_record();
break;
case “view_record”:
view_record();
break;
}
以上代码是典型的如何处理用户通过HTTP GET方法发送过来的编辑与查询请求的。这里其实我们就在响应用户的超链接或者按钮事件,(或者可能js触发的ajax调用)。
注意到这些系统边界的处理总是有着极大的重复性,而且当系统不断壮大时我们要不停的增加这些映射是多么繁琐的事情,而且我们在违反DRY(don't repeat yourself 不要重复你自己);
在OOP领域对事件驱动有着相当成熟的模式可以用,设计模式的宗旨之一就是不要重复发明轮子,所以可以直接拿来用了。在上面函数式的事件处理轮廓中有两个角色,一个就是事件分发器(由swith语句充当),另一个就是事件响应器(由那两个函数来承当)再有一个就是case后面的东西 那个就是事件!!!。
看这个URl:http://myserver/interface.php?event=edit 这个URL就明显的表明用户的意图,要系统对edit事件进行响应,实际上现在好多开源项目的url设计都有很多这种思路,比如act ,ac前缀代表的就是命令/事件 后台会找对应的处理脚本,你在康盛公司的项目中仔细观察URL的规律就可看出某些东西。所以设计其实可以从URL开始的,URL就是接口,这个在面向对象中可认为是面向接口编程:举个例子,你想编辑某个书籍的信息,你就可以先设计URL,如:myserver/book.php?act=edit&bid=334455 ,想查看某个书籍的信息就可以 myserver/book.php?act=detail&bid=334455。 等依次类推,这也可认为是URL驱动的设计。
事件分发器的职责是来根据事件查询事件响应器的,并把处理流程传递给事件响应器,他本身并不做事件处理工作。
所有的事件响应器都在做类似的事情:从$_GET,$_POST(或者是$_REQUEST)中提取用户的请求参数,并做某些操作(文件系统,网络,或数据库相关的工作)然后返回处理结果。OO中很重要的一个特性就是继承,继承可以带来代码重用的好处:把公共的东西提取到父类中,子类只实现特定于自己的功能,这样就不用你不停的代码复制,也就符合了DRY原则了。
看看事件整个生命周期:用户动作导致事件————》事件从网络上传递到系统内部————》事件被事件分发器捕获————》事件分发器查询该事件对应的处理器————》事件分发器把流程转给响应的事件响应器————》事件响应器处理事件————》返回处理结果。
以上流程都是正常流,当然可能发生没有注册事件处理器或异常情况。以上的事件也就仅是一个字符串而已(当然事件的信息都在POST GET和COOKIE中)。
***
****
事件框架图:
上面的图抽象类没有画getPdo方法,还有参数问题,自己看着办吧,可要可不要反正php中参数信息都在全局数组中放着,全局数组($_GET,$_POST,$_COOKIE,$_REQUEST,$_SERVER,$_SESSION就是整个应用的通信总线);
事件分发器需要具有注册和注销事件处理器的能力。所有的事件处理器都实现了同一个接口,为了方便在中间又引入了一个抽象类,这个类有个pdo属性,这个属性是子类共用的,唯独handle方法还需要被实例化(被具体类来实现)。
这样设计的目的是规范化编码,php是弱类型的语言,只要你实现了某个方法,就可以认为你是某个接口的实现者,这个要求比较宽泛,但为了严格起见,还是声明自己实现了某个接口,注册时往往也会类型检查的!。
类细说:
class Dispatcher
{
private $event;
function __construct($eventStr){
$this- >event = $eventStr;
}
function handleEvent(){
$eventReactorClass = “{$this- >event}_Handler”;
if (class_exists($eventReactorClass )){
$handler_obj = new $eventReactorClass ($this- >event);
$response = $handler_obj->handle();
return $response;
}else{
echo “I can’t handle this!”;
}
}
}
上面的代码主要是根据代表事件的字符串来查找对应的事件响应类,并实例化一个事件处理对象,之后调用其方法handle,返回处理的结果即可。这里有很多策略可以用比如注册事件字串跟类的映射关系,事件处理器类所在的文件夹等都可以考虑,当然“惯例优于配置”已经被好多项目所接纳,所以所有的事件响应器类都以事件名很后缀_Handler结尾,方便了开发。所以现在流行的MVC框架都有一套命名惯例。
要扩充系统功能,只需要实现事件处理器的那个handle函数即可,类名中暗含有要处理哪个事件的信息。至于该函数传递的参数,可以选择性用之,也可以忽略。当然一般事件分发器会把最完备的信息作为参数传递给事件响应器的。常常自定义一个Request Response类作为参数传入,Request即封装$_GET,$_POST,$_COOKIE或者$_REQUEST数组。Response作为响应数据的收集容器用 一般带有缓存性质。
IEventHandle接口类:
interface IEventHandle {
function handle();
}
提供通讯协议,让所有的子类都遵从此协议(就是实现那个handle方法)。
抽象类 EventHandler:
abstract class EventHandler
{
private $pdo;
function getPdo(){
$this->pdo = new PDO('配置串');
return $pdo;
}
abstract function handle();
}
主要为子类提供便捷的获取pdo或者数据库连接句柄的功能,这样就不用在子类中重复出现获取数据库连接这段代码。
当然可以根据项目需要随意添加公共资源在这个抽象类中。具体事件处理函数的实现还是推迟到子类中了。
具体事件响应器类:
class Delete_Handler extends EventHandler{
private $event;
function __construct($event){
$this->event = $event;
}
//下面实现本类要完成的任务:参数看情况弄吧
function handle(...){
$targetId = $_REQUEST['id'];//获取删除的目标ID
$pdo = parent::getPdo();
try{
//用pdo完成删除记录的操作
}catch(PDOException $ex){
throw $ex;
}
//或者用模板技术显示某个页面去
return true;
}
}
****
*****
引入安全:
一个操纵是否被允许执行,需要参照当前的上下文信息,上下文也指环境,即当前是哪个行为者在触发事件;
常规的实现是这样的:
performSomeMethod(){
$operator = $_SESSION['user'];
if(!empty($operator) && $operator == 'someRole'){
//这里执行常规代码;
}eles{
echo "你无权进行此项操作!";
}
}
以上代码中主要编入了验证逻辑,判断用户是否登录,并且是某个角色,然后符合要求后再执行正常的执行逻辑,如果这样的逻辑确实是每个事件响应器必须执行的那么可以提取到抽象类中,并重新设计一个方法叫:secureHandle()
使之为抽象的强迫子类实现,并改写分发器:改为调用这个secureHandle()方法,这时接口可能也要改,在php中接口可能只是一个约定而已,如果不进行类型检查那么接口看似可有可无。这样的方案个人感觉不是太好,所以给出一个我认为还可以接受的方案:
改写抽象类:
abstract class EventHandler
{
private $pdo;
function getPdo(){
$this->pdo = new PDO('配置串');
return $pdo;
}
//使用模板方法设计模式
public function handle($eventContext){
try{
//如果有参数就传进来
$this->_before($eventContext);
$response = $this->_handle($eventContext);
$this->_after($eventContext);
return $response;
}catch(Exception $ex){
//是重抛还是如何处理取决于你的异常处理策略
}
}
protected funciton _before(&$_context){
//这里做验证 不通过 就抛一个验证失败的异常。
}
protected function _after(&$_context){
//这里随便做啥 或者日志吧!
}
//强迫子类实现这个方法
abstract function _handle(&$_context);
}
经过以上改写,应用了模板方法设计模式 可以把验证提到_before操作中,把_handle方法设计为抽象的强迫子类实现
如果子类还想更改安全验证策略只需要覆写_before操作即可。这样原先的接口,事件分发器,都不需要改动。另外这里也用到了before/after设计模式。请自己查找相关资料。现在子类唯一要做的是复写_handle方法,必要时复写_before方法。
*****
**************
结语:
还有许多问题值得考虑,这里只给出了大概的思路,比如GUI领域中常常出现的事件注册机制是否需要,主要用来根据事件查找对应的处理器,当然这个映射完全可以来自配置文件,或者数据库。如果结合上访问控制列表技术,每一个用户都有一个对应的可操作的事件处理器集,可以根据当前用户Id加载其所有可用的事件处理器列表,然后再调用相应处理器的方法,如果列表中找不到对应于事件的处理器证明当前用户没有这个权利。这个逻辑就可以提取到抽象类中来实现。
另一个值得考虑的问题是类加载问题,一个方法是把系统可能用到的类文件提前全部include进来,这在小项目中是可行的,但是对于第三方库或者有多个文件夹且相互嵌套时 这种方法会很笨拙。另一种方法是在事件分发器加载类之前从一个配置文件中读取相关类和类所在文件的映射信息,然后把类文件包含进来。还有一个就是zendFramework用的把文件夹加进类路径中include_path然后使用自动加载spl_autoload技术。我前面有一个自己写的自动加载类可以用的!
*
事件驱动在桌面型应用中是非常普遍的,比如你点击鼠标,点击某个按钮应用程序就得对你的动作做出相应的反应,从程序员的角度看,有两个角色需要识别:一个是用户 你个就是你构建的系统,此外无它! 现在用中间者的身份看二者的行为模式:用户总是在向系统发送某种信息,这种信息促使系统进行响应,所有的请求从更抽象的角度观察,无外乎两种:命令与请求;请严格区分这两者,命令是促使系统做某种变更(比如删除一条记录),而请求是需要系统返回某些信息(比如查询);当然命令也一般会反馈一些信息告诉系统执行情况(是成功了还是失败了,还是发生了不可预知的异常。);从这里可以得到一个适应所有情况的编程接口
execute(IN paramInformation):returnedInformation; //参数和返回值有时可选
事件模型是对 “变更——响应”机制的实现,一种机制总是可用多个方法来实现,这取决于编程者如何想问题的。php内部并没有所谓的事件,事件分发器的概念,但并不代表我们不能用事件模型来处理php世界里的问题!!
*
**
理解事件:
什么是事件,从你易接受的角度看就是键盘,鼠标等人物动作引起的变更,这个变更就叫事件:鼠标单击事件,鼠标拖动事件,键盘点击事件...... 。 从我们人类对世界的认识角度来看:所有对你,或对大家有意义的变更就是事件,比如918事件,1212事件911事件,自然界无时不刻发生着某种变化,而只有这些变化引起你足够重视时他才是事件,比如你可以说1980年发生了一个重大事件---那一天我诞生了(这对你父母来说当然是重大事件,至少比911重大些);所以最后阐明一点:事件就是变化,任何变化的结果都能被定义为事件,所以什么时事件有你来决定。
从计算机角度看,所有的01序列的位变化都可定义为事件,仍取决于你,但计算机最底层的实现无外乎读与写,所以读和写性质的操作结果(或者这个发生)就可以被称为事件。还有一个比较特殊的变化是时间,从四维空间来看虽然三维空间上的某些东西没有变,但时间维总是在向前推进,所有时间也可以引起事件。
任何变更或条件的满足都能被定义为事件,如果不产生信息交换,一个封闭的系统对外界不会产生有意义的事件,你如果只设计了一个没有键盘和鼠标的电脑,估计电脑充其量也就是一个看着复杂的电视或DVD而已。为此程序都向外部暴露了某些接口,通过这些接口我们就能够触发某些变更。这些接口可以是GUI上的可视化接口,也可以是命令行方式的接口,当然从仿形递归角度看,更小一些的组件(子系统,类..)会向外界暴露一些API。
外界暴露的接口是通向系统内部的引脚,也是触发系统内部事件的导火索,在web开发中这些接口,如你所见比如超链接,按钮,输入框,下拉菜单等,这些东西都能引发系统对其响应。从上面的事件理解来看,事件是可以被忽略的(比如911对别人来说是事件对我就不是,又不会影响我什么),而且要紧的一点是事件可以传播!!变化是可以导致变化的,所以一个事件就有可能导致另一个事件。这些都是事件的特征。值得一提的事想想如果形成环行事件传播路径会怎样??
**
***
我们是系统的设计者,从系统角度来看我们需要响应用户对系统的请求与命令。也就是需要响应一些事件,然后做某些处理,把最终的执行情况或请求的信息返回给客户。在系统的边界处我们要把这些事件跟系统内部预定义好的响应模块映射起来:
switch($_GET[‘action’]) {
case “edit_record”:
edit_record();
break;
case “view_record”:
view_record();
break;
}
以上代码是典型的如何处理用户通过HTTP GET方法发送过来的编辑与查询请求的。这里其实我们就在响应用户的超链接或者按钮事件,(或者可能js触发的ajax调用)。
注意到这些系统边界的处理总是有着极大的重复性,而且当系统不断壮大时我们要不停的增加这些映射是多么繁琐的事情,而且我们在违反DRY(don't repeat yourself 不要重复你自己);
在OOP领域对事件驱动有着相当成熟的模式可以用,设计模式的宗旨之一就是不要重复发明轮子,所以可以直接拿来用了。在上面函数式的事件处理轮廓中有两个角色,一个就是事件分发器(由swith语句充当),另一个就是事件响应器(由那两个函数来承当)再有一个就是case后面的东西 那个就是事件!!!。
看这个URl:http://myserver/interface.php?event=edit 这个URL就明显的表明用户的意图,要系统对edit事件进行响应,实际上现在好多开源项目的url设计都有很多这种思路,比如act ,ac前缀代表的就是命令/事件 后台会找对应的处理脚本,你在康盛公司的项目中仔细观察URL的规律就可看出某些东西。所以设计其实可以从URL开始的,URL就是接口,这个在面向对象中可认为是面向接口编程:举个例子,你想编辑某个书籍的信息,你就可以先设计URL,如:myserver/book.php?act=edit&bid=334455 ,想查看某个书籍的信息就可以 myserver/book.php?act=detail&bid=334455。 等依次类推,这也可认为是URL驱动的设计。
事件分发器的职责是来根据事件查询事件响应器的,并把处理流程传递给事件响应器,他本身并不做事件处理工作。
所有的事件响应器都在做类似的事情:从$_GET,$_POST(或者是$_REQUEST)中提取用户的请求参数,并做某些操作(文件系统,网络,或数据库相关的工作)然后返回处理结果。OO中很重要的一个特性就是继承,继承可以带来代码重用的好处:把公共的东西提取到父类中,子类只实现特定于自己的功能,这样就不用你不停的代码复制,也就符合了DRY原则了。
看看事件整个生命周期:用户动作导致事件————》事件从网络上传递到系统内部————》事件被事件分发器捕获————》事件分发器查询该事件对应的处理器————》事件分发器把流程转给响应的事件响应器————》事件响应器处理事件————》返回处理结果。
以上流程都是正常流,当然可能发生没有注册事件处理器或异常情况。以上的事件也就仅是一个字符串而已(当然事件的信息都在POST GET和COOKIE中)。
***
****
事件框架图:
上面的图抽象类没有画getPdo方法,还有参数问题,自己看着办吧,可要可不要反正php中参数信息都在全局数组中放着,全局数组($_GET,$_POST,$_COOKIE,$_REQUEST,$_SERVER,$_SESSION就是整个应用的通信总线);
事件分发器需要具有注册和注销事件处理器的能力。所有的事件处理器都实现了同一个接口,为了方便在中间又引入了一个抽象类,这个类有个pdo属性,这个属性是子类共用的,唯独handle方法还需要被实例化(被具体类来实现)。
这样设计的目的是规范化编码,php是弱类型的语言,只要你实现了某个方法,就可以认为你是某个接口的实现者,这个要求比较宽泛,但为了严格起见,还是声明自己实现了某个接口,注册时往往也会类型检查的!。
类细说:
class Dispatcher
{
private $event;
function __construct($eventStr){
$this- >event = $eventStr;
}
function handleEvent(){
$eventReactorClass = “{$this- >event}_Handler”;
if (class_exists($eventReactorClass )){
$handler_obj = new $eventReactorClass ($this- >event);
$response = $handler_obj->handle();
return $response;
}else{
echo “I can’t handle this!”;
}
}
}
上面的代码主要是根据代表事件的字符串来查找对应的事件响应类,并实例化一个事件处理对象,之后调用其方法handle,返回处理的结果即可。这里有很多策略可以用比如注册事件字串跟类的映射关系,事件处理器类所在的文件夹等都可以考虑,当然“惯例优于配置”已经被好多项目所接纳,所以所有的事件响应器类都以事件名很后缀_Handler结尾,方便了开发。所以现在流行的MVC框架都有一套命名惯例。
要扩充系统功能,只需要实现事件处理器的那个handle函数即可,类名中暗含有要处理哪个事件的信息。至于该函数传递的参数,可以选择性用之,也可以忽略。当然一般事件分发器会把最完备的信息作为参数传递给事件响应器的。常常自定义一个Request Response类作为参数传入,Request即封装$_GET,$_POST,$_COOKIE或者$_REQUEST数组。Response作为响应数据的收集容器用 一般带有缓存性质。
IEventHandle接口类:
interface IEventHandle {
function handle();
}
提供通讯协议,让所有的子类都遵从此协议(就是实现那个handle方法)。
抽象类 EventHandler:
abstract class EventHandler
{
private $pdo;
function getPdo(){
$this->pdo = new PDO('配置串');
return $pdo;
}
abstract function handle();
}
主要为子类提供便捷的获取pdo或者数据库连接句柄的功能,这样就不用在子类中重复出现获取数据库连接这段代码。
当然可以根据项目需要随意添加公共资源在这个抽象类中。具体事件处理函数的实现还是推迟到子类中了。
具体事件响应器类:
class Delete_Handler extends EventHandler{
private $event;
function __construct($event){
$this->event = $event;
}
//下面实现本类要完成的任务:参数看情况弄吧
function handle(...){
$targetId = $_REQUEST['id'];//获取删除的目标ID
$pdo = parent::getPdo();
try{
//用pdo完成删除记录的操作
}catch(PDOException $ex){
throw $ex;
}
//或者用模板技术显示某个页面去
return true;
}
}
****
*****
引入安全:
一个操纵是否被允许执行,需要参照当前的上下文信息,上下文也指环境,即当前是哪个行为者在触发事件;
常规的实现是这样的:
performSomeMethod(){
$operator = $_SESSION['user'];
if(!empty($operator) && $operator == 'someRole'){
//这里执行常规代码;
}eles{
echo "你无权进行此项操作!";
}
}
以上代码中主要编入了验证逻辑,判断用户是否登录,并且是某个角色,然后符合要求后再执行正常的执行逻辑,如果这样的逻辑确实是每个事件响应器必须执行的那么可以提取到抽象类中,并重新设计一个方法叫:secureHandle()
使之为抽象的强迫子类实现,并改写分发器:改为调用这个secureHandle()方法,这时接口可能也要改,在php中接口可能只是一个约定而已,如果不进行类型检查那么接口看似可有可无。这样的方案个人感觉不是太好,所以给出一个我认为还可以接受的方案:
改写抽象类:
abstract class EventHandler
{
private $pdo;
function getPdo(){
$this->pdo = new PDO('配置串');
return $pdo;
}
//使用模板方法设计模式
public function handle($eventContext){
try{
//如果有参数就传进来
$this->_before($eventContext);
$response = $this->_handle($eventContext);
$this->_after($eventContext);
return $response;
}catch(Exception $ex){
//是重抛还是如何处理取决于你的异常处理策略
}
}
protected funciton _before(&$_context){
//这里做验证 不通过 就抛一个验证失败的异常。
}
protected function _after(&$_context){
//这里随便做啥 或者日志吧!
}
//强迫子类实现这个方法
abstract function _handle(&$_context);
}
经过以上改写,应用了模板方法设计模式 可以把验证提到_before操作中,把_handle方法设计为抽象的强迫子类实现
如果子类还想更改安全验证策略只需要覆写_before操作即可。这样原先的接口,事件分发器,都不需要改动。另外这里也用到了before/after设计模式。请自己查找相关资料。现在子类唯一要做的是复写_handle方法,必要时复写_before方法。
*****
**************
结语:
还有许多问题值得考虑,这里只给出了大概的思路,比如GUI领域中常常出现的事件注册机制是否需要,主要用来根据事件查找对应的处理器,当然这个映射完全可以来自配置文件,或者数据库。如果结合上访问控制列表技术,每一个用户都有一个对应的可操作的事件处理器集,可以根据当前用户Id加载其所有可用的事件处理器列表,然后再调用相应处理器的方法,如果列表中找不到对应于事件的处理器证明当前用户没有这个权利。这个逻辑就可以提取到抽象类中来实现。
另一个值得考虑的问题是类加载问题,一个方法是把系统可能用到的类文件提前全部include进来,这在小项目中是可行的,但是对于第三方库或者有多个文件夹且相互嵌套时 这种方法会很笨拙。另一种方法是在事件分发器加载类之前从一个配置文件中读取相关类和类所在文件的映射信息,然后把类文件包含进来。还有一个就是zendFramework用的把文件夹加进类路径中include_path然后使用自动加载spl_autoload技术。我前面有一个自己写的自动加载类可以用的!