基于CI的事件驅(qū)動(dòng)擴(kuò)展和開發(fā)規(guī)范
問(wèn)題
項(xiàng)目中為了追求速度和性能,數(shù)據(jù)庫(kù)的表設(shè)計(jì)往往不是滿足范式的。這就可能導(dǎo)致在改一個(gè)表中項(xiàng)目實(shí)體的元信息時(shí),需要同時(shí)修改其他表中的信息。比方說(shuō):我有一個(gè)一張表來(lái)表示虛擬的文件(每一行記錄表示一個(gè)文件),另一個(gè)張表用來(lái)記錄已經(jīng)發(fā)布的文件和生成的外鏈信息??赡転榱松龠M(jìn)行一次查表,我們會(huì)把文件的一些基本信息,如(文件名,發(fā)布人的名字)記錄在外鏈的表中。當(dāng)修改了文件表中的元信息時(shí),外鏈表中的信息也需要修改。常見的方法是使用ORM,但如果我還需要“根據(jù)具體情況再?zèng)Q定要修改其他表中的元信息”這種情況時(shí),ORM就有點(diǎn)難搞了。
同時(shí),我希望我在對(duì)上一個(gè)問(wèn)題中提到的“文件”數(shù)據(jù)進(jìn)行操作時(shí),不需要知道任何其他相關(guān)的細(xì)節(jié)。也就是將其他的這些關(guān)系劃到其他模塊去。
系統(tǒng)的接口往往需要復(fù)合的權(quán)限控制,并且在完成基礎(chǔ)的部分的權(quán)限控制之后,不希望由于后續(xù)功能的增加而去修改基礎(chǔ)部分。同時(shí)希望后續(xù)的這些功能在不啟用時(shí),系統(tǒng)能夠恢復(fù)到基礎(chǔ)的權(quán)限控制策略。比方說(shuō),一個(gè)模擬的網(wǎng)盤文件,我在系統(tǒng)沒有增加分享這個(gè)功能時(shí),權(quán)限控制策略是“只有自己可以訪問(wèn)”,在增加了分享功能后,策略是“指定分享的好友都可以訪問(wèn)”。為了在單獨(dú)完成分享模塊代碼時(shí)不修改之前的代碼,常用的方法是使用鉤子來(lái)拋出權(quán)限信息,系統(tǒng)自動(dòng)進(jìn)行復(fù)合,下面會(huì)詳述。
當(dāng)項(xiàng)目不是太大時(shí)(沒有大到需要使用HMVC等更高級(jí)的模式),需要一種簡(jiǎn)單、弱耦合的模塊管理和開發(fā)規(guī)范。
解決方案(以CI為基礎(chǔ)框架)
第一部分
以 問(wèn)題1和2 中的例子來(lái)說(shuō),數(shù)據(jù)變化的主體是文件,其他都是跟隨變化。很自然就讓人想到觀察者模式,只不過(guò)我這里不是把“關(guān)聯(lián)”的類注冊(cè)到“文件”類中監(jiān)聽變化,而是聲明一個(gè)全局事件,每一個(gè)模塊都持有它的一個(gè)引用,都通過(guò)它來(lái)拋出事件,都通過(guò)監(jiān)聽它的事件來(lái)進(jìn)行自己的操作。
你可能會(huì)說(shuō)這在某種程度上破壞了模塊的封裝,因?yàn)槟K知道了上層的細(xì)節(jié)。但是這樣做就大大降低了模塊之間的耦合。首先,基礎(chǔ)模塊(“文件”)不用知道外部如何應(yīng)對(duì)變化,也不用管理外部的監(jiān)聽者,對(duì)自己的操作只需要拋出一個(gè)事件就夠了。對(duì)監(jiān)聽模塊來(lái)說(shuō),只需要監(jiān)聽系統(tǒng)統(tǒng)一約定的事件就好,設(shè)置不用關(guān)注基礎(chǔ)模塊的監(jiān)聽方法甚至名字都不用關(guān)心。
在CI中的實(shí)現(xiàn)有兩個(gè)步驟:
1.在CI中聲明一個(gè)事件類,生成一個(gè)實(shí)例作為全局事件對(duì)象,綁定在控制器實(shí)力上。
2.使用CI的model作為模塊(為了實(shí)現(xiàn)更強(qiáng)的封裝可以把業(yè)務(wù)邏輯單獨(dú)寫成libraries中的類),初始化時(shí)給它綁定這個(gè)事件。同時(shí)獲取的模塊需要監(jiān)聽的事件,將這些事件綁定到全局事件對(duì)象。
以下是代碼,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 的構(gòu)造函數(shù)實(shí)現(xiàn):
- public function __construct(){
- parent::__construct();
- //初始化事件中心
- $this -> load -> library("Event");
- //初始化注冊(cè)模塊,這里寫你自己的。
- $modules = array('user','test');
- //初始化事件中心模塊
- $auths = array();
- foreach( $modules as $module ){
- //初始化各個(gè)模塊,將事件中心傳入以供模塊調(diào)用
- $model_name = "{$module}_model";
- $this -> load -> model( $model_name, $module );
- $this -> $model_name -> event = $this -> event;
- //以上這句優(yōu)雅一點(diǎn)可以寫成
- //$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 );
- }
- }
以上你注意到模塊需要有一個(gè)listen方法,來(lái)返回所有自己需要監(jiān)聽的事件。如果你不喜歡這種約定也可以在模塊獲得全局事件對(duì)象event后,自己在模塊內(nèi)通過(guò)event->bind()來(lái)實(shí)現(xiàn)綁定。
以下是listen返回的事件監(jiān)聽數(shù)組,也是事件格式:
- public function listen(){
- return array(
- //事件名 => 觸發(fā)的函數(shù)名
- "user logged in" => "react_user_login"
- );
- }
第二部分
對(duì)于事件的復(fù)合我采用了一個(gè)簡(jiǎn)單的鉤子模式,就是讓模塊約定聲明一個(gè)auth方法,返回自己要進(jìn)行權(quán)限控制的api和自己進(jìn)行控制的方法。示例如下:
- public function auth(){
- return array(
- //api名稱
- 'main/index' => array(
- //權(quán)限規(guī)則名稱
- 'user_login' => array(
- //對(duì)同一api需要忽略掉的規(guī)則
- 'ignore' => array( 'text_login' ),
- //自己的驗(yàn)證函數(shù)
- 'validate' => 'login_validate'
- )
- )
- );
- }
由于一個(gè)api可能會(huì)有多個(gè)模塊聲明自己的驗(yàn)證規(guī)則,所以提供一個(gè)ignore字段來(lái)表示需要明確忽略掉的規(guī)則。在validate指向的函數(shù)值,函數(shù)自己通過(guò)post或這個(gè)get獲取參數(shù)并進(jìn)行驗(yàn)證。這里有點(diǎn)讓人感覺不舒服的地方就是上層的模塊需要知道基礎(chǔ)模塊的權(quán)限驗(yàn)證細(xì)節(jié),以便使用ignore來(lái)去掉和自己沖突的規(guī)則。好在這種情況應(yīng)該不會(huì)太多,大部分可以通過(guò)“將沖突的api拆成不同的api”來(lái)解決。而且這種方法可以使你在增加功能時(shí)完全不再修改之前的權(quán)限設(shè)置。
那么如何進(jìn)行合并?這里改造了一下MY_controller。代碼如下:
- class MY_Controller extends CI_Controller{
- protected $auth_array = array();
- public function __construct(){
- parent::__construct();
- //初始化事件中心
- $this -> load -> library("Event");
- //初始化注冊(cè)模塊
- $modules = array('user','test');
- //初始化事件中心模塊
- $auths = array();
- foreach( $modules as $module ){
- //初始化各個(gè)模塊,將事件中心傳入以供模塊調(diào)用
- $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 );
- //獲取模塊的權(quán)限信息
- if( method_exists( $this -> $module , "auth") ){
- $auths[$module] = $this -> $module -> auth() ;
- }
- }
- //得到整合后的權(quán)限數(shù)組
- $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(){
- //獲取當(dāng)前路徑
- $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']();
- }
- }
- }
控制器將最后計(jì)算出來(lái)的權(quán)限驗(yàn)證數(shù)組放在了自己的auth_array屬性中,用戶在繼承了該控制器之后,通過(guò)$this -> auth_validate() 就能開始執(zhí)行驗(yàn)證。
如果你不喜歡這種控制器與權(quán)限合并的方式或者你的控制器很復(fù)雜時(shí),你也可以將權(quán)限單獨(dú)提出到一個(gè)類中。另外你可以再權(quán)限合并函數(shù)中記錄日志幫助調(diào)試。
另外貼出兩個(gè)具體的model:
- <?php
- /**
- * @author rainer_H
- * @date 2012-6-26
- * @encode UTF-8
- */
- class User_model extends CI_Model{
- //聲明自己的權(quán)限控制規(guī)則
- public function auth(){
- return array(
- //api名稱
- 'main/index' => array(
- //權(quán)限規(guī)則名稱
- 'user_login' => array(
- //對(duì)同一api需要忽略掉的規(guī)則
- 'ignore' => array( 'text_login' ),
- //自己的驗(yàn)證函數(shù)
- 'validate' => 'login_validate'
- )
- )
- );
- }
- public function __construct( ){
- parent::__construct( );
- }
- //聲明自己需要監(jiān)聽的對(duì)象
- 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.";
- }
- }
- ?>
原文鏈接:http://www.cnblogs.com/sskyy/archive/2012/06/27/2565294.html
【編輯推薦】