最近在做一個網盤的項目,得到了很多經驗和教訓。總結了一些常見的問題,於是寫了下面這樣一個小東西來解決。
問題
- 項目中為了追求速度和性能,數據庫的表設計往往不是滿足范式的。這就可能導致在改一個表中項目實體的元信息時,需要同時修改其他表中的信息。比方說:我有一個一張表來表示虛擬的文件(每一行記錄表示一個文件),另一個張表用來記錄已經發布的文件和生成的外鏈信息。可能為了少進行一次查表,我們會把文件的一些基本信息,如(文件名,發布人的名字)記錄在外鏈的表中。當修改了文件表中的元信息時,外鏈表中的信息也需要修改。常見的方法是使用ORM,但如果我還需要“根據具體情況再決定要修改其他表中的元信息”這種情況時,ORM就有點難搞了。
- 同時,我希望我在對上一個問題中提到的“文件”數據進行操作時,不需要知道任何其他相關的細節。也就是將其他的這些關系划到其他模塊去。
- 系統的接口往往需要復合的權限控制,並且在完成基礎的部分的權限控制之后,不希望由於后續功能的增加而去修改基礎部分。同時希望后續的這些功能在不啟用時,系統能夠恢復到基礎的權限控制策略。比方說,一個模擬的網盤文件,我在系統沒有增加分享這個功能時,權限控制策略是“只有自己可以訪問”,在增加了分享功能后,策略是“指定分享的好友都可以訪問”。為了在單獨完成分享模塊代碼時不修改之前的代碼,常用的方法是使用鈎子來拋出權限信息,系統自動進行復合,下面會詳述。
- 當項目不是太大時(沒有大到需要使用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(skyking_H@hotmail.com)。謝謝。