CodeIgniter 框架分析
入口文件
入口文件主要完成下列工作:
1) 指定 CodeIgniter 框架所在目錄;
2) 定義 APPPATH 常量,指示應用程序文件根目錄;
3) 載入 codeigniter/CodeIgniter.php 文件,啟動框架。
codeigniter/CodeIgniter.php 文件
這個文件是 CodeIgniter 的基本文件,主要完成初始化 CodeIgniter 框架和啟動應用程序兩項工作。
1) 實例化 CI_Benchmark,這個類用于標記應用程序執行消耗的時間;
2) 載入應用程序的配置文件 require(APPPATH.'config/config'.EXT);
3) 實例化 CI_Config,這個類用于將數組封裝為可以操作的配置服務;
4) 實例化 CI_Router,這個類用于分析 URL 請求,確定要執行的控制器和動作;
5) 實例化 CI_Output,這個類提供輸出內容的緩存和檢查服務;
6) 通過 $OUT->_display_cache($CFG, $RTR) 嘗試輸出緩存內容,如果成功,則結束程序運行;
7) 判斷控制器類定義文件是否存在。如果不存在則通過 show_404() 顯示錯誤信息;
8) 實例化 CI_Input,這個類提供對 $_GET、$_POST 的訪問手段,并封裝了一些過濾方法;
9) 實例化 CI_URI,這個類提供對 URL 的分析、構造服務;
10) 實例化 CI_Language,這個類提供多語言字符串映射服務;
11) 載入 codeigniter/Base4.php 或者 codeigniter/Base5.php;
12) 載入 libraries/Controller.php;
13) 載入控制器類定義文件;
14) 實例化控制器類;
15) 如果控制器使用了 scaffolding 功能,則調用控制器的 _ci_scaffolding() 方法,否則調用控制器動作方法;
16) 通過 $OUT->_display(); 輸出內容($OUT 是 CI_Output 的實例)。
CI_Benchmark
這個類很簡單,就是用 microtime() 函數記錄時間點,并提供 elapsed_time() 方法來計算兩個時間點之間消耗的時間。這個類功能不多,但是很實用。CodeIgniter 中大部分類都是這種設計思想,值得稱贊!
CI_Config
這個類其實是在內部維護了一個數組,用來記錄應用程序的設置(類似 Windows 注冊表)。這種簡單的封裝可以強制應用程序按照固定的規范訪問設置,同時又不將設置保存為全局變量,避免無意中遭到破壞或篡改。
CI_Router
CI_Router 功能很單一。CI_Router 首先分析出應用程序當前使用的 URL 模式:PATHINFO 或普通模式。接下來從 URL 地址中分析出控制器名字、動作名以及參數名和參數值。分析的結果保存為 CI_Router 對象實例的成員變量。
這里比較有特點的是,CI_Router 可以根據開發者在應用程序設置里面定義的模式來分析 URL,而不是使用某種固定的模式。
CI_Output
CI_Output 有兩個主要功能:獲得應用程序執行的所有輸出內容和輸出緩存服務。
應用程序執行的輸出結果都會保存為 CI_Output 的成員變量。然后根據應用程序設置,CI_Output::_display() 方法會調用 CI_Output::_write_cache() 方法將輸出內容緩存起來。下一次當使用 CI_Output::_display_cache() 時如果緩存已經建立了,并且沒有過期,則會直接輸出緩存內容。
在 CI_Output::_write_cache() 中,是根據 URL 地址和 URL 參數來確定緩存 ID 的。因此即便是同一個控制器和動作,只要使用不同的 URL 參數,也會緩存不同的內容。
這個類的功能很簡單,因此在許多動態頁面是無法使用的。例如用戶登錄前和登錄后,訪問同一個控制器和動作并使用相同的 URL 參數,頁面內容也有可能是不同的。這時,CI_Output 的緩存就不能使用。
因為從本質上來說,CI_Output 提供的緩存是在應用程序之外的,所以應用程序無法根據當前狀態來決定是否緩存頁面。當一個頁面被緩存后,對該頁面的訪問實際上根本就不會執行應用程序代碼,而是由 CI_Output 取出緩存內容直接就輸出了。
CI_Input
CI_Input 是類似過濾器,并且提供了對 $_GET、$_POST 的封裝服務。例如用 CI_Input::post() 方法來訪問 $_POST。由于多了這層封裝,CI_Input 可以在 post() 方法中對數據進行更多的過濾。
這種封裝從出發點上看,是很不錯的。但是這也會造成一些問題。例如 CI_Input 只有在調用 post() 方法時才能進行過濾。如果應用程序使用 $_POST 直接獲取數據,那么實際上就繞過了安全屏障。如果應用程序使用了第三方庫,那么這種風險更大,因為第三方庫很可能會直接使用 $_POST 等全局變量。
因此有些開發者認為過濾應該是全局的,即在框架初始化時,就對所有輸入數據進行過濾。但初始化時的全局過濾靈活性很差,要么全過濾,要么都不過濾,沒法做到對個別數據的單獨過濾。
CI_Input 的另一個問題,就是沒有處理 magic_quotes。不管 magic_quotes 設置為什么,CI_Input 都沒有對數據進行相關的處理。這樣一來,如果服務器的 magic_quotes 設置不同,那么應用程序得到的數據也是不一致的。后來查看數據庫驅動的代碼,發現 CI_Input 將對 magic_quotes 的處理放到了數據庫驅動中。
這種設計是有很大缺陷的!如果應用程序取得數據后,并不是存入數據庫(例如直接顯示或存入文件),那么就必須自行判斷 magic_quotes 的狀態。這種判斷不但煩瑣,而且容易遺忘。所以框架有責任將所有數據庫整理為一致的格式,要么是應用 addslashes() 轉義過后的數據,要么是沒有轉義的數據。
奇怪的是 CI_Input 卻對輸入數據的字段名進行了 magic_quotes 檢查,并應用了 addslashes()。這是為了讓數據庫字段名不會成為 SQL 注入攻擊的根源。甚至,CI_Input 還會將 \n\r\n\r 替換為 \n。這種隨意篡改原始數據的做法,非常不可取。
總之,我個人認為 CodeIgniter 在這部分的設計是很糟糕的。不過要改善也很簡單,幾行代碼就可以了。然后修改一下數據庫驅動。但是由于已經有許多采用 CodeIgniter 開發的應用程序,所以這樣的升級改動,影響是非常大的。
CI_URI
由于 CodeIgniter 允許應用程序定義 URL 映射模式,所以需要專門的工具來生成 URL 地址。CI_URI 就是完成這些工作的。
CI_Language
這個類可以載入不同的語言文件。然后應用程序就可以用 CI_Language::line() 方法取出某個項目的對應翻譯。每個語言文件就是一個名值對數組。所以 CI_Language::line() 以項目名做為鍵名,就可以查詢到對應的翻譯。
codeigniter/Base
codeigniter/Base4.php 和 codeigniter/Base5.php 功能一樣,只不過分別適用于 PHP4 和 PHP5 而已。其中定義了 CI_Base 類和一個非常重要的 get_instance() 函數。
get_instance() 函數返回一個 CI_Base 類在整個應用程序中的唯一實例。
這里有一個有趣的發現。Base4.php 和 Base5.php 中的 CI_Base 和 get_instance() 有這完全不同的實現。
在 Base4.php(對應 PHP4)中,CI_Base 直接繼承自 CI_Loader。CI_Base 實例化時,將 自身的引用保存到了 CI_Base::$load 中。也就是說 CI_Base 實例的 $load 實際上指向自己。然后 $load 被復制到一個名為 $OBJ 的全局變量。
在 PHP4 版的 get_instance() 函數中,如果檢查到 $CI(這是 CI_Base 的實例,也就是控制器的實例)存在,就返回 $CI,否則返回全局變量 $OBJ->load。但由于在 PHP4 中,$OBJ->load 實際上就是一個 CI_Base 的實例。所以。。。。所以。。。。。。還是返回了一個 CI_Base 的實例。真搞不懂作者為什么這樣寫,簡直要讓人發瘋。
不管怎么樣,應用程序其他地方調用 get_instance() 都會獲得一個 CI_Base 的實例。
在 Base5.php(對應 PHP5)中,用一個 singleton 模式來解決了這個問題。因此 CI_Base 也不再需要從 CI_Loader 繼承了。不過這也留下了隱患(CI_Loader 實例要什么時候獲取呢?),所以在 CI_Base 的繼承類 Controller 中,只好通過判斷是否是運行 PHP5 來決定是不是要實例化一個 CI_Loader。
真的很無語啊,這種設計雖然可以用,但是很糟糕。在 PHP4 種,CI_Loader 的方法和成員變量暴露在了 CI_Base 中。如果應用程序不小心調用了這些方法或使用了這些成員變量。那么應用程序在 PHP5 中運行就會出錯。
Controller
Controller 類是所有控制器的基礎類。Controller 實例化時會將 CI_Input、CI_Benchmark、CI_Config、CI_URI、CI_Output、CI_Language 的實例復制到 Controller 實例的成員變量中。然后根據應用程序設置,自動載入文件。
但是這里作者顯然沒有處理好,所以不得不用 global $IN, $BM, $CFG, $URI, $LANG, $OUT;
這樣的全局變量來傳遞幾個重要的對象實例。
Controller 本身并沒提供 model、helper 的載入服務。這些都由 CI_Loader 來提供。但是,CI_Loader 的各種載入服務,卻又用 get_instance() 獲取控制器的實例,然后調用 Controller(控制器都是 Controller 的繼承類哦)的 _ci_initialize()、_ci_init_database() 等方法來做初始化。
神啊!救救我吧!這種錯綜復雜的關系,真的要人命啊!
Controller 的 $ci_is_loaded 成員變量用于保存已經載入的對象實例。所以每次用 Controller::_ci_load_model() 載入模塊后,都要將該模塊登記到 $ci_is_loaded,以避免重復載入。
Controller 里面大部分是一些初始化各種服務的方法,例如初始化數據庫、Model 的方法。還有就是用 _ci_scaffolding() 調用 CodeIgniter 的“腳手架”功能。
對 Controller 的設計,沒什么好說的,一個字:爛!
CI_Loader
CI_Loader 提供各種載入服務,例如載入 Model、Helper、View 等。但是(我真的很痛恨“但是”這個詞),CI_Loader 卻需要 Controller 來完成初始化。那么又是誰來調用 CI_Loader 呢?答案是 Controller。
這種緊密的耦合,完全是沒有必要的!
控制器開始執行
分析到這里,終于進入應用程序的代碼了。應用程序控制器中,可以用 $this->load 來載入各種服務,然后就可以調用這些載入的服務了。
雖然 CodeIgniter 在 CI_Base、Controller 和 CI_Loader 上設計很糟糕,但開發者如果不在乎這些,那么開發過程還是很愉快的。
下面我們再來看看 CodeIgniter 主要服務的特點。
數據庫訪問
與大部分框架不同,CodeIgniter 的 Model 類沒有提供數據庫訪問功能。所有數據庫操作都是通過數據庫驅動程序來進行的。
所有數據庫驅動均繼承自 CI_DB 類。等等,我怎么找不到 CI_DB 類的定義呢?因為 CI_DB 類是在 Controller 中用 eval('class CI_DB extends CI_DB_driver { }');
這行代碼來定義的。定義這樣一個空殼,估計是作者為以后擴充數據庫驅動留下的伏筆。
CodeIgniter 的數據庫驅動,功能都很簡單,和 AdoDB Lite 類似,但是缺乏 AdoDB Lite 那么多的擴展庫。我個人認為反倒不如用 AdoDB Lite 來替換這部分。當然了,CodeIgniter 目前已經有不少數據庫驅動了,所以替換成 AdoDB Lite 好處不多。
CodeIgniter 也提供了一個 ActiveRecord 實現,不過這個 ActiveRecord 可沒有一點半點的“ORM”能力。但是 CodeIgniter 的 ActiveRecord 不需要為每一個數據表都構造一個實例。通常一個實例就可以處理多個數據表的操作。例如 $query = $this->db->get('mytable');
和 $query = $this->db->get('mytable2');
就可以分別取得 mytable 和 mytable2 的數據。
說實話,作者可能用錯了名字。CodeIgniter 中的“ActiveRecord”實際上是表數據入口模式——TableDataGateway。
CodeIgniter 中的 ActiveRecord 基本上只是一個對數據表進行 CRUD 操作的公共接口。沒有提供 RoR、CakePHP、FleaPHP 等框架具有的數據表關聯自動處理能力。和自己寫 SQL 相比,沒什么優勢。唯一的好處就是作者所說的可以讓 ActiveRecord 來生成這些簡單的 SQL 語句,而不用自己寫,提高應用程序在不同數據庫之間移植的能力。
“腳手架”功能
CodeIgniter 中提供了基本的“腳手架”功能,可以用幾行代碼即實現一個對某個數據表進行 CRUD 的界面。這和 phpMyAdmin 中的數據瀏覽、編輯頁面類似,當然功能要簡單得多。
“腳手架”有什么實用價值,眾說紛紜。但普遍認同的一點就是“腳手架”功能為處于開發初期的應用程序提供了管理數據的界面。開發者可以在后期替換掉“腳手架”的界面。
但是,CodeIgniter 也太簡單了,就只有 CRUD 操作,還不如 phpMyAdmin 好用。
其他
CodeIgniter 還有許多其他的類和助手。這些類基本上都屬于提供各種輔助服務的范疇。有些類很不錯,像圖片操作。但大部分類和助手實在太簡單,缺乏實用價值。像數據驗證助手,只能做很基本的驗證,在絕大多數應用程序里面都不能滿足要求。
總結
咳——咳——,總結時間到了。
再次鄭重申明:本文所有文字均為作者個人理解和感想。作者盡量做到客觀,但人非圣賢,難免參雜個人好惡在其中。所以如果你看到不爽的文字,請自動無視,謝謝合作!
CodeIgniter 是一個:簡單不簡潔、好用但可能不夠用的工具。
幾個步驟就可以讓你的應用程序跑起來,所以簡單。因為簡單,所以好用。但糟糕的設計增加了復雜度,簡單的表面下是錯綜復雜的對象關系。因為過于簡單,所以可能不夠用。
如果你只是開發很簡單的應用程序,那么 CodeIgniter 完全可以滿足你的需求。而且你也會獲得愉快的體驗。
但如果應用程序具有一定的復雜度,CodeIgniter 就可能起到反作用。因為 CodeIgniter 在幾個主要類上的糟糕設計,你的應用程序最終也會受到牽連。而且 CodeIgniter 缺乏許多必須的服務,例如訪問控制、用戶管理、自動化的數據表關聯處理、復雜緩存等。
這些服務對于一個較為復雜的應用程序來說都是必須的。如果用 CodeIgniter 作為應用程序框架,那么這些服務都需要自己實現。這時 CodeIgniter 帶來的好處就很少了。