基于CodeIgniter的事件驱动扩展和开发规范

最近在做一个网盘的项目,得到了很多经验和教训。总结了一些常见的问题,于是写了下面这样一个小东西来解决。


问题


  1. 项目中为了追求速度和性能,数据库的表设计往往不是满足范式的。这就可能导致在改一个表中项目实体的元信息时,需要同时修改其他表中的信息。比方说:我有一个一张表来表示虚拟的文件(每一行记录表示一个文件),另一个张表用来记录已经发布的文件和生成的外链信息。可能为了少进行一次查表,我们会把文件的一些基本信息,如(文件名,发布人的名字)记录在外链的表中。当修改了文件表中的元信息时,外链表中的信息也需要修改。常见的方法是使用ORM,但如果我还需要“根据具体情况再决定要修改其他表中的元信息”这种情况时,ORM就有点难搞了。
  2. 同时,我希望我在对上一个问题中提到的“文件”数据进行操作时,不需要知道任何其他相关的细节。也就是将其他的这些关系划到其他模块去。
  3. 系统的接口往往需要复合的权限控制,并且在完成基础的部分的权限控制之后,不希望由于后续功能的增加而去修改基础部分。同时希望后续的这些功能在不启用时,系统能够恢复到基础的权限控制策略。比方说,一个模拟的网盘文件,我在系统没有增加分享这个功能时,权限控制策略是“只有自己可以访问”,在增加了分享功能后,策略是“指定分享的好友都可以访问”。为了在单独完成分享模块代码时不修改之前的代码,常用的方法是使用钩子来抛出权限信息,系统自动进行复合,下面会详述。
  4. 当项目不是太大时(没有大到需要使用HMVC等更高级的模式),需要一种简单、弱耦合的模块管理和开发规范。

 

解决方案(以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])。谢谢。

你可能感兴趣的:(CodeIgniter)