最近在做一个网盘的项目,得到了很多经验和教训。总结了一些常见的问题,于是写了下面这样一个小东西来解决。
问题
解决方案(以CI为基础框架)
第一部分
以 问题1和2 中的例子来说,数据变化的主体是文件,其他都是跟随变化。很自然就让人想到观察者模式,只不过我这里不是把“关联”的类注册到“文件”类中监听变化,而是声明一个全局事件,每一个模块都持有它的一个引用,都通过它来抛出事件,都通过监听它的事件来进行自己的操作。
你可能会说这在某种程度上破坏了模块的封装,因为模块知道了上层的细节。但是这样做就大大降低了模块之间的耦合。首先,基础模块(“文件”)不用知道外部如何应对变化,也不用管理外部的监听者,对自己的操作只需要抛出一个事件就够了。对监听模块来说,只需要监听系统统一约定的事件就好,设置不用关注基础模块的监听方法甚至名字都不用关心。
在CI中的实现有两个步骤:
1.在CI中声明一个事件类,生成一个实例作为全局事件对象,绑定在控制器实力上。
2.使用CI的model作为模块(为了实现更强的封装可以把业务逻辑单独写成libraries中的类),初始化时给它绑定这个事件。同时获取的模块需要监听的事件,将这些事件绑定到全局事件对象。
以下是代码,Event 类。
<?php /** * @author rainer_H * @date 2012-6-25 * @encode UTF-8 */ class Event { private $event_array = array(); public function __construct(){ } //$module_callback : array(module_name, callback_method) public function bind( $event_name, $module_callback ){ if( !isset( $this -> event_array[$event_name] ) ){ $this -> event_array[$event_name] = array(); } array_push( $this -> event_array[$event_name], $module_callback ); } public function multi_bind( &$bindings ){ foreach( $bindings as $event_name => $module_callback ){ $this -> bind( $event_name, $module_callback ); } } public function trigger( $event_name ){ if( isset( $this -> event_array[$event_name] )){ foreach( $this -> event_array[$event_name] as $module_callback ){ $args = array_slice( func_get_args(), 1); call_user_func_array(array( $module_callback[0],$module_callback[1]), $args); } } } } ?>
MY_Controller 的构造函数实现:
public function __construct(){ parent::__construct(); //初始化事件中心 $this -> load -> library("Event"); //初始化注册模块,这里写你自己的。 $modules = array('user','test'); //初始化事件中心模块 $auths = array(); foreach( $modules as $module ){ //初始化各个模块,将事件中心传入以供模块调用 $model_name = "{$module}_model"; $this -> load -> model( $model_name, $module ); $this -> $model_name -> event = $this -> event; //以上这句优雅一点可以写成 //$this -> $model_name -> set_handler($this -> event); //绑定事件 $listen = $this -> $module -> listen(); foreach( $listen as $event_name => $callback ){ $listen[$event_name] = array( $this-> $module, $callback ); } $this -> event -> multi_bind( $listen ); } }
以上你注意到模块需要有一个listen方法,来返回所有自己需要监听的事件。如果你不喜欢这种约定也可以在模块获得全局事件对象event后,自己在模块内通过event->bind()来实现绑定。
以下是listen返回的事件监听数组,也是事件格式:
public function listen(){ return array( //事件名 => 触发的函数名 "user logged in" => "react_user_login" ); }
第二部分
对于事件的复合我采用了一个简单的钩子模式,就是让模块约定声明一个auth方法,返回自己要进行权限控制的api和自己进行控制的方法。示例如下:
public function auth(){ return array( //api名称 'main/index' => array( //权限规则名称 'user_login' => array( //对同一api需要忽略掉的规则 'ignore' => array( 'text_login' ), //自己的验证函数 'validate' => 'login_validate' ) ) ); }
由于一个api可能会有多个模块声明自己的验证规则,所以提供一个ignore字段来表示需要明确忽略掉的规则。在validate指向的函数值,函数自己通过post或这个get获取参数并进行验证。这里有点让人感觉不舒服的地方就是上层的模块需要知道基础模块的权限验证细节,以便使用ignore来去掉和自己冲突的规则。好在这种情况应该不会太多,大部分可以通过“将冲突的api拆成不同的api”来解决。而且这种方法可以使你在增加功能时完全不再修改之前的权限设置。
那么如何进行合并?这里改造了一下MY_controller。代码如下:
class MY_Controller extends CI_Controller{ protected $auth_array = array(); public function __construct(){ parent::__construct(); //初始化事件中心 $this -> load -> library("Event"); //初始化注册模块 $modules = array('user','test'); //初始化事件中心模块 $auths = array(); foreach( $modules as $module ){ //初始化各个模块,将事件中心传入以供模块调用 $model_name = "{$module}_model"; $this -> load -> model( $model_name, $module ); $this -> $model_name -> event = $this -> event; //绑定事件 $listen = $this -> $module -> listen(); foreach( $listen as $event_name => $callback ){ $listen[$event_name] = array( $this-> $module, $callback ); } $this -> event -> multi_bind( $listen ); //获取模块的权限信息 if( method_exists( $this -> $module , "auth") ){ $auths[$module] = $this -> $module -> auth() ; } } //得到整合后的权限数组 $this -> auth_array = $this -> map_auth_array( $auths ); } private function map_auth_array( $auth_array ) { $output = array(); foreach( $auth_array as $module_name => $auths_content ){ foreach( $auths_content as $route => $auths ){ if( !isset( $output[$route] ) ){ $output[$route] = array(); $output[$route]['ignore'] = array(); } foreach( $auths as $auth_name => $auth ){ $auths[$auth_name]['module'] = $module_name; if( isset( $auth['ignore'] ) ){ if( !is_array( $auth['ignore'])){ $auth['ignore'] = array( $auth['ignore'] ); } $output[$route]['ignore'] = array_merge($output[$route]['ignore'],$auth['ignore']); array_unique( $output[$route]['ignore'] ); } } $output[$route] += $auths; } } foreach( $output as $route => $auths){ if( !empty( $auths['ignore'] ) ){ foreach( $auths['ignore'] as $ignore ){ unset( $output[$route][$ignore] ); } } unset( $output[$route]['ignore']); } return $output; } public function auth_validate(){ //获取当前路径 $route = 'main/index'; if( $this -> auth_array[$route] && !empty( $this -> auth_array[$route] ) ){ foreach( $this -> auth_array[$route] as $auth ){ $this -> $auth['module'] -> $auth['validate'](); } } } }
控制器将最后计算出来的权限验证数组放在了自己的auth_array属性中,用户在继承了该控制器之后,通过$this -> auth_validate() 就能开始执行验证。
如果你不喜欢这种控制器与权限合并的方式或者你的控制器很复杂时,你也可以将权限单独提出到一个类中。另外你可以再权限合并函数中记录日志帮助调试。
另外贴出两个具体的model:
<?php /** * @author rainer_H * @date 2012-6-26 * @encode UTF-8 */ class User_model extends CI_Model{ //声明自己的权限控制规则 public function auth(){ return array( //api名称 'main/index' => array( //权限规则名称 'user_login' => array( //对同一api需要忽略掉的规则 'ignore' => array( 'text_login' ), //自己的验证函数 'validate' => 'login_validate' ) ) ); } public function __construct( ){ parent::__construct( ); } //声明自己需要监听的对象 public function listen(){ return array( ); } public function login_validate(){ echo "user login_validate"; } public function login(){ $this -> event -> trigger( "user logged in", "hahaha" ); } } ?>
<?php /** * @author rainer_H * @date 2012-6-26 * @encode UTF-8 */ class Test_model extends CI_Model{ public function __construct( ){ parent::__construct( ); } public function auth(){ return array( 'main/index' => array( 'text_login' => array( 'validate' => 'login_validate' ) ) ); } public function listen(){ return array( "user logged in" => "react_user_login" ); } public function login_validate(){ echo "test login_validate"; } public function react_user_login( $user = false ){ echo "{$user} user logged in react from Test."; } } ?>
总结
总的来说这套方法是从前端开发中借鉴来的。希望能在中小项目开发中起来一些作用。我会继续更新它,如果你有任何意见和建议都请给我留言或者email([email protected])。谢谢。