YII 框架源碼分析javascript
百度聯盟事業部——黃銀鋒php
目 錄css
一、 引言 3html
1.二、本文內容與結構 3java
二、組件化與模塊化 4jquery
2.三、組件 6ajax
2.5 、App 應用 10
2.6 、WebApp 應用 11
4.1 、Action 23
4.2 、Filter 24
5.1 、DAO 層 30
5.1.3 、Command 對象 31
5.2.1 、Command 構造器 33
5.3.2 、單表 ORM 34
5.3.3 、多表 ORM 36
5.3.4 、CModel 與 CValidator 37
一、引言
1.一、Yii 簡介
Yii 的做者是美籍華人「薛強」,他原是 Prado 核心開發成員之一。2008 年薛強另起爐竈, 開發了 Yii 框架,於 2008 年 12 月 3 日發佈了 Yii1.0 版本。
Yii 是目前比較優秀的 PHP 框架之一,它的支持的特性包括:MVC、DAO/ActiveRecord、 I18N/L10N、caching、AJAX 支持、用戶認證和基於角色的訪問控制、腳手架、輸入驗證、部 件、事件、主題化以及 Web 服務等。
Yii 的不少思想參考了其它一些比較優秀的 Web 框架(咱們寫東西時是否是也喜歡參考別人的? 有木有?嘿嘿,都喜歡站在別人的肩膀上幹活!),下面是一個簡短的列表:
框架名稱 |
參考思想 |
Prado |
基於組件和事件驅動編程模式、數據庫抽象 層、模塊化的應用架構、國際化和本地化等 |
Ruby on Rails |
配置思想、基於 Active Record 的 ORM |
jQuery |
集成了 jQuery |
Symfony |
過濾設計和插件架構 |
Joomla |
模塊化設計和信息翻譯方案 |
1.二、本文內容與結構
本文對 Yii1.1.8 版本的源代碼進行了深刻的分析,本文的內容與結構爲: 組件化與模塊化:對 Yii 的基於組件和事件驅動編程模式的基礎類(CComponent)進行分
析;對組件化和模塊化的工做原理進行分析;對 WebApp 應用建立 Controller 流程等進行分 析。
系統組件:對 Yii 框架自帶的重要組件進行分析,主要包括:日誌路由組件、Url 管理組 件、異常處理組件、Cache 組件、基於角色的訪問控制組件等。
控制器層:控制器主要包含 Action 和 Filter,對 Action 與 Filter 的工做原理進行分析。 模型層:對 DAO 層、元數據和 Command 構造器、ORM 的原理進行分析
視圖層:對視圖層的渲染過程、Widget 和客戶端腳本組件等進行分析
本文檔中的錯誤或不妥之處在所不免,殷切但願本文檔的讀者給予批評指正!
二、組件化與模塊化
2.一、框架加載和運行流程
start
加載YiiBase.php 一、安裝autoload方法, 爲類的實例化作準備 二、得到框架全部類的路 徑(Yii1.1.8共208類)
根據ActionId建立Action對象 一、從成員函數中尋找Action 二、從ActionMap中尋找Action拋加載request組件(Get,Post,Cookie)建立Action 否 異 是否成功? 常
建立WebApp實例 一、初始化別名:application、 webroot、ext 二、安裝異常處理句柄,拋異常時 交給異常處理組件來處理 三、配置核心組件:urlManager、 errorHandler、session、db等 四、根據配置信息來配置組件、子 模塊、WebApp成員屬性等 五、加載preload組件:log組件、 request組件等
運行WebApp
一、觸發onBeginRequest事件
加載Url管理組件 根據配置信息分析url,解析出路 由:route=ControllerId/ActionId
根據ControllerId建立控制器 一、從ControllerMap中尋找 二、從子模塊中尋找 三、從ControllerPath中尋找
建立控制器 拋 是否成功? 否 異常根據filters()配置,建立出當 前Action的全部Filter對象運行Filter1的preFilter方法 運行Filter2的preFilter方法檢查Get參 拋 數與Action 否 異 的參數否常一致運行ActionPartial render渲染出核心部分的html
Layout render
渲染出總體的html
Client Script render 嵌入javascript、css生 成最終的html |
|
|
|
echo html
二、處理請求
運行控制器 運行Filter2的postFilter方法 運行Filter1的postFilter方法
三、觸發onEndRequest事件
註冊的句柄:
異常處理 組件
XX處拋異常
throw Exception。。。
end
好比記錄日誌
Yii 框架加載和運行流程共分 4 個階段(也許看着有點嚇人,木有關係,咱們先知道一個大概):
Step1:WebApp 初始化與運行
1.一、 加載 YiiBase.php,安裝 autoload 方法;加載用戶的配置文件;
1.二、 建立 WebApp 應用,並對 App 進行初始化,加載部分組件,最後執行 WebApp
Step2:控制器初始化與運行
2.一、 加載 request 組件,加載 Url 管理組件,得到路由信息 route=ControllerId/ActionId 2.二、 建立出控制器實例,並運行控制器
Step3:控制器初始化與運行
3.一、 根據路由建立出 Action
3.二、 根據配置,建立出該 Action 的 Filter; 3.三、 執行 Filter 和 Action
Step4:渲染階段
4.一、 渲染部分視圖和渲染布局視圖
4.二、 渲染註冊的 javascript 和 css
2.二、YiiBase 靜態類
YiiBase 爲 YII 框架的運行提供了公共的基礎功能:別名管理與對象建立管理。 在建立一個 php 的對象時,須要先 include 這個類的定義文件,而後再 new 這個對象。
在不一樣環境下(開發環境/測試環境/線上環境),apache 的 webroot 路徑的配置可能不同, 因此這個類的定義文件的全路徑就會不一樣,Yii 框架經過 YiiBase 的別名管理來解決了這個問 題。
在 創 建 對象時 , 需 要導入 對應 類的定義 , 常常需 要 使 用這 5 個 函數 : include()、 include_once()、require()、require_once()、set_include_path()。Yii 經過使用 YiiBase::import() 來統一解決這個問題。下圖描述了 YiiBase 提供「別名管理與對象建立管理」的工做原理:
經過createComponent建立對象
一、若是類不存在,則經過import導入
經過new建立對象
二、new這個對象
三、根據輸入對這個對象的屬性初始化
import導入一個類的定義導入一個路徑到include_path autoload若是類是別名打頭的,經過別管管理接口得到全路徑別名管理getPathOfAlias setPathOfAlias添加一個別名的全路徑
首先看別名管理,它是經過爲某個文件夾(一個文件夾每每對應一個模塊)起一個別名, 在 YII 框架中可使用這個別名來替代這個文件夾的全路徑,好比:system 別名表明的是框 架 /home/work/yii/framework 的 路 徑 , 所 以 可 以 使 用 system.base.CApplication 表明
/home/work/yii/framework/base/CApplication.php 文件的路徑。固然在應用層(咱們)的代碼中 也能夠經過 Yii::setPathOfAlias 來註冊別名。
通常狀況下咱們使用絕對路徑或者相對路徑來進行文件引用,固然這 2 種狀況都有弊端。 絕對路徑:當咱們的代碼部署到測試環境或者線上環境的時候須要大量修改被 include 文件 的路徑;相對路徑:當某些模塊的文件夾的位置發生調整(更名)的時候,全部的相對路徑都 須要修改。而使用別名的方式只須要改一處:註冊別名的時候,即 Yii::setPathOfAlias()。從 而將文件夾的變更而致使的代碼改動集中到一處完成。
再看 import 功能:a、導入一個類的定義,從而能夠建立該類的對象;b、將某個文件夾 加入到 include_path,從而能夠直接 include 這個文件下的全部文件。Yii::import 至關於以下
5 個函數的統一:include()、include_once()、require()、require_once()、set_include_path()。 並且通常狀況下速度會比這些函數更快。固然 Yii::import 支持別名的功能,從而能夠解決路 徑變更帶來的麻煩。
最後看一下對象的建立,在 YII 框架中有 2 中方法建立對象:一、使用 new 關鍵字;二、
使用 Yii::createComponent 方法。
當使用 new 關鍵字建立對象時,autoload 會分 3 步來尋找對應類的定義:a、判斷是否 爲 framework 中的類(framework 的全部類和這個類的全路徑都保存在 YiiBase 的一個成員變 量中,就至關於整個框架都 import 了);二、判斷是否使用 Yii::import 導入了這個類,對於非 框架的類,咱們在建立這個類的對象時須要先 import 這個類的定義;三、從 include_path 目 錄下查找以這個類名字命名的 php 腳本,因此在開發的時候類名儘可能與文件名保存一致, 這樣咱們導入(import)包含這個文件的文件夾就好了,從而無需把這個文件夾中的每一個文件 都導入一遍。
當使用 Yii::createComponent 方法建立對象時,它提供了比 new 關鍵字更多的功能:a、 經過這個類的全路徑別名來指定類的位置和類名(類名必須與文件名一致),當這個類尚未 導入的時候,會根據全路徑來自動導入這個類的定義;二、對建立出來的對象的成員變量進 行賦值。即以下圖描述,原來要寫 3 行以上的代碼,如今一行代碼就能夠搞定(write less, do more)。
Yii::import('application.models.Student');
$obj = new Student();
$obj->age = 16;
$obj->name = 'jerry';
$obj = Yii::createComponent(array( 'class'=>'application.models.Student', 'age'=>16,
'name'=>'jerry'
));
2.三、組件
CComponent 類就是組件,它爲整個框架的組件編程和事件驅動編程提供了基礎,YII 框架中的大部分類都將 CComponent 類做爲基類。CComponent 類爲它的子類提供 3 個特性: 一、成員變量擴展
經過定義兩個成員函數(getXXX/setXXX)來定義一個成員變量,好比:
public function getText() {…} public function setText {…}
這樣就至關於定義了一個 text 成員變量,能夠這樣調用
$a=new CComponent;
$a=$component->text; // 等價於$a=$component->getText();
$component->text='abc'; // 等價於$component->setText('abc');
CComponent 是經過魔術方法 get 和 set 來實現「成員變量擴展」特性的,若是對類 自己不存在的成員變量進行操做時,php 會調用這個類的 get 和 set 方法來進行處理。 CComponent 利用這兩個魔術方法實現了「成員變量擴展」特性。下圖描述了一個 CComponent 的子類,它增長了 active 和 sessionName 兩個成員變量,該圖描述了對於這兩個成員變量的 調用流程。
getActive
setActive
是否存在這個 成員變量
否 set()
get()
getSessionName
setSessionName
是 使用本對象的成員變量
getXXX setXXX
面向對象編程中直接定義一個成員變量就能夠了,爲何 CComponent 要經過定義 2 個 函數來實現一個成員變量呢?一個主要得緣由是須要對成員變量進行「延時加載」,通常情 況下類的成員變量是在構造函數或者初始化函數進行統一賦值,可是在一次 web 請求的處 理過程當中不是每一個成員變量都會被使用,好比 App 類中定義了兩個成員變量:$cache 和$db
($cache 是一個緩存對象,$db 是一個數據庫連接對象),這兩個對象在 App 類初始化的時
候建立,可是一個 web 網站的有些頁面,它內容能夠經過緩存獲取,那麼數據庫連接對象 其實就不須要建立。若是將 App 定義爲 CComponent 的子類,在 App 類中定義兩個方法: getCache/getDb,這樣就能夠作到第一次使用 db 成員變量的時候,才調用 getDb 函數來進行 數據庫連接的初始化,從而實現延時加載——即在第一次使用時進行初始化。雖然延時加載 會增長一次函數調用,可是能夠減小沒必要要的成員變量的初始化(整體上實際上是提高了網站 的訪問速度),並且可使得咱們的代碼更加易維護、易擴展。
延時加載應該是「成員變量擴展」特性的最重要的用途,固然這個特性還會有其它用途, 想想,當你操做一個成員變量的時候,你實際上是在調用 getXXX 和 setXXX 成員函數,你是 在調用一段代碼!
二、事件模型
事件模型就是設計模式中的「觀察者模式」:當對象的狀態發生了變化,那麼這個對象 能夠將該事件通知其它對象。
爲了使用事件模型,須要實現這三個步驟:一、定義事件;二、註冊事件句柄;三、觸發 事件。
CComponent 的子類經過定義一個以 on 打頭的成員函數來定義一個事件,好比:public function onClick(){…},接着經過調用 attachEventHandler 成員函數來註冊事件句柄(能夠註冊 多個事件句柄),最後經過調用 raiseEvent 來觸發事件。
attachEventHandler detachEventHandler raiseEvent
事件句柄容器
onclick
fun_11 fun_12
„ fun_1n
beforeinsert
fun_2n
afterinsert
fun_3n
„„
Key
fun_m1 |
|
|
Value
fun_mn
CComponent 類使用一個私有的成員變量來保存事件以及處理該事件的全部句柄,該成 員變量能夠看做一個 hash 表,hash 表的 key 是事件的名稱,hash 表的 value 是事件處理函 數鏈表。
三、行爲類綁定
有兩種辦法能夠對類添加特性:一、直接修改這個類的代碼:添加一些成員函數和成員 變量;二、派生:經過子類來擴展。很明顯第二種方法更加易維護、易擴展。若是須要對一 個類添加多個特性(多人在不一樣時期),那麼須要進行多級派生,這顯然加大了維護成本。 CComponent 使用一種特殊的方式對類信息擴展——行爲類綁定。行爲類是 CBehavior 類的一個子類,CComponent 能夠將一個或者多個 CBehavior 類的成員函數和成員變量添加
到本身身上,而且在不須要的時候卸載掉某些 CBehavior 類。下面是一個簡單的例子:
//計算器類
class Calculator extends CBehavior
{
public function add($x, $y) { return $x + $y; } public function sub($x, $y) { return $x - $y; }
...
}
$comp = new CComponent();
//爲個人類添加計算器功能
$comp->attachbehavior('calculator', new Calculator());
$comp->add(2, 5);
$comp->sub(2, 5);
CComponent 經過 get、 set 和 call 這 3 個魔術方法來實現「行爲類綁定」這個特性, 當調用 CComponent 類不存在的成員變量和成員方法的時候,CComponent 類會經過這三個 魔法方法在「動態綁定的行爲對象」上進行查找。即將不存在的成員變量和成員方法路由到 「動態綁定對象」上。
attachBehavior detachBehavior
綁定一個對象 解除綁定
obj1
set()
obj2
是否不存在
否 get()
查詢各個對象
call()
obj3
是
使用本對象的成員 變量和成員函數
„
綁定的對象
使用綁定對象流程 綁定的維護流程
能夠用 3 句話來總結 CComponent 類的特性:
一、 更好的配置一個對象,當設置對象的成員變量的時候,實際上是運行一段代碼;
二、 更好的監聽一個對象,當對象的內部狀態發生變化的時候,其它對象能夠獲得通知;
三、 更好的擴展一個對象,能夠給一個對象增長成員變量和成員函數,還能監聽這個對 象的狀態。
2.四、模塊
模塊是整個系統中一些相對獨立的程序單元,完成一個相對獨立的軟件功能。好比 Yii 自帶的 gii 模塊,它實現了在線代碼生成的功能。CModule 是全部模塊類的基類,它有 3 部 分組成:
a、基本屬性(模塊 id,模塊路徑等); b、組件,這是模塊的核心組成部分,模塊能夠當作這些組件的容器; c、子模塊,這爲模塊提供了擴展性,好比一個模塊作大了,能夠拆成多個子模塊(每一個
子模塊也是有這 3 部分組成,是一個遞歸結構)。 下圖是模塊與它的成員之間的包含關係圖:
模板基
本屬性 組件 子模塊
下表列出了 CModule 各個組成部分:
3 部分 |
詳細成員 |
說明 |
基本屬性 (用戶對整個模塊的全局性 的東西進行配置) |
id |
模塊的 id |
parentModule |
父模塊 |
|
basePath |
當前模塊的路徑 |
|
modulePath |
子模塊的路徑 |
|
params |
模塊的參數 |
|
preload |
須要預先加載的組件 id |
|
behaviors |
綁定的行爲類 |
|
aliases |
新增長的別名,添加到 YiiBase 的別名管理中 |
|
import |
須要包含的文件或者路徑 |
|
組件 (這是模塊的核心組成部分) |
components |
數組類型,數組的每一個成員描述了一個組件 |
子模塊 (這爲模塊提供了擴展性) |
modules |
數組類型,數組的每一個成員描述了一個模塊, 每一個模塊也是有這 3 部分組成,是遞歸結構 |
能夠很是方便的對模塊的這 3 個組成部分進行初始化:使用一個數組進行配置,數組的 key 是須要配置的屬性,value 就是須要配置的值,下圖是一個例子,爲何會如此方面的進 行配置呢?由於 CModule 繼承自 CComponent 類,因此在對成員屬性進行配置的時候,其實 是在運行一段代碼,即一個成員函數。
array(
'basePath'=>dirname( FILE ).DIRECTORY_SEPARATOR.'..',//模塊的路徑 'preload'=>array('log'),//須要預先加載日誌組件
'import'=>array('application.models.*', 'application.components.*',),//須要include的路徑
//組件的配置
'components'=>array( 'user'=>array(//用戶組件的配置
'allowAutoLogin'=>true
),
'log'=>array(//日誌組件的配置 'class'=>'CLogRouter',
'routes'=>array(array('class'=>'CWebLogRoute','levels'=>'trace, profile'))
)
),
//模塊的配置
'modules'=>array(
'gii'=>array(//自動生成代碼模塊的配置 'class'=>'system.gii.GiiModule', 'password'=>'123456'
),
),
);
2.5 、App 應用
應用是指請求處理中的執行上下文。它的主要任務是分析用戶請求並將其分派到合適的 控制器中以做進一步處理。它同時做爲服務中心,維護應用級別的配置。鑑於此,應用也叫 作「前端控制器」。
Yii 使用 CApplication 類用來表示 App 應用,CApplication 繼承自 CModule,它在父類基 礎上作了 3 方面的擴展:一、增長一個 run 方法;二、添加了若干成員屬性;三、添加了若干 組件。
run 方法的做用至關於 C 語言的 main 函數,是整個程序開始運行的入口,內部調用虛 函數 processRequest 來處理每一個請求,CApplication 有 2 個子類:CWebApplication 和 CConsoleApplication,它們都實現了該方法。在處理每一個請求的開始和結束分別發起了 onBeginRequest 和 onEndRequest 事件,用於通知監聽的觀察者。複習一下「Yii 框架加載和 運行流程」圖,從中能夠找到該方法在整個流程中所起的做用。
添加的成員變量、成員函數和組件見下表:
類別 |
名稱 |
說明 |
成員變量 |
name |
應用的名稱 |
charset |
應用的編碼集,默認爲 UTF-8 |
|
sourceLanguage |
編碼所使用的語言和區域 id 號,這在開發多語言時須要, 默認爲 UTF-8 |
|
language |
app 要求的語言和區域 id 號,默認爲 sourceLanguage |
|
runtimePath |
運行時的路徑,好比全局的狀態會保存到這個路徑下,默 認爲 application.runtime |
|
extensionPath |
放第三方擴展的路徑,默認爲 application.ext |
|
timezone |
獲取或者設置時區 |
|
locale |
本地化對象,用於對時間、數字等的本地化 |
|
globalsate |
全局狀態數組,該數組會被持久化(經過 statePersister 實現) |
|
組件 |
coreMessages |
對框架層內容進行翻譯,支持多語言 |
messages |
對應用層內容進行翻譯,支持多語言 |
|
db |
數據庫組件 |
errorHandler |
異常處理組件,該組件與 App 配合來處理全部的異常 |
|
securityManager |
安全管理組件 |
|
statePersister |
狀態持久化組件 |
|
urlManager |
url 管理組件 |
|
request |
請求組件 |
|
format |
格式化組件 |
2.6 、WebApp 應用
每一個 web 請求都由 WebApp 應用來處理,即 WebApp 應用爲 http 請求的處理提供了運 行的環境。WebApp 應用就是 CWebApplication 類,它的最主要工做是根據 url 中的路由來創 建對於的控制類,下圖描述了控制器建立的過程,主要由 3 步組成:
一、在成員變量 controllerMap 中查找,判斷是否有對應的 Controller,controllerMap 的 優先級最高
二、在子模塊中中查找,判斷是否有對應的 Controller 三、在 ControllerPath 及其子文件夾中查找
搜索控制器的過程
輸入的路由爲:seg1/seg2/seg3
調用createController(‘seg1/seg2/seg3’,$app)
1
在controllerMap中尋 找id爲seg1的控制類
調用createController(‘seg2/seg3’,
$subModule)
2 不存在
遞歸調用
在id爲seg1的子模塊 中尋找 |
|
|
|
不存在
3
在ControllerPath路 徑下逐層尋找
是否存在ControllerPath/seg1/Seg2Controller.php ControlId爲/seg1/seg2
不存在 是否存在ControllerPath/seg1/seg2/Seg3Controller.php
ControlId爲/seg1/seg2/seg3
添加的重要的成員變量、成員函數和組件見下表:
類別 |
名稱 |
說明 |
成員變量 |
defaultController |
默認的控制類,若是沒有指定控制器,則使用該控制器 |
|
layout |
默認的佈局,若是控制器沒有指定佈局,則使用該佈局 |
|
controllerMap |
控制器映射表,給某些特定的路由指定控制器 |
|
theme |
設置主題 |
|
controller |
當前的控制器對象 |
|
controllerPath |
控制器文件的路徑 |
|
ViewPath |
視圖層文件的路徑,默認爲 protected/views/ |
|
SystemViewPath |
系統視圖文件的路徑,默認爲 protected/views/system/ |
|
LayoutPath |
佈局文件的路徑,默認爲 protected/views/layouts/ |
組件 |
session |
session 組件 |
|
assetManager |
資源管理組件,用於發佈私有的 js、css 和 image |
|
user |
用戶組件,用戶登陸等 |
|
themeManager |
主題組件 |
|
authManager |
權限組件,實現了基於角色的權限控制 |
|
clientScript |
客戶端腳本管理組件,管理 js 和 css 代碼 |
三、系統組件
3.一、日誌路由組件
每一個 Web 系統在運行的過程當中都須要記錄日誌,日誌能夠記錄到文件或數據庫中,在 開發階段能夠把日誌直接輸出到頁面得底部,這樣能夠加快開發速度。Yii 在日誌處理上作 了以下 2 點重要工做:
一、每一個 Http 請求,可能須要記錄多條日誌(數據庫更新日誌/與其它系統交互日誌)。比 如某次 Http 請求要記錄 18 條日誌,咱們是每記一條日誌都寫一次硬盤(即寫 18 硬盤)呢,還 是在請求快結束的時候一次性寫硬盤?很顯然,先把這些日誌保存在一個 php 的數組中, 在請求快結束的時候,把數組中的全部日誌一次性寫硬盤速度要快一些。
二、每條日誌能夠從 2 個維度來進行分類:日誌的嚴重級別、日誌的業務邏輯。用下表
來描述「百度創意專家」產品的日誌在這 2 個維度上的狀況:
業務邏輯 嚴重級別 |
數據庫日誌 |
用戶中心接口日誌 |
Drmc 接口日誌 |
Memcache 日誌 |
trace |
|
|
|
|
info |
|
|
|
|
profile |
|
|
|
|
warning |
|
|
|
|
error |
|
|
|
|
按業務邏輯分爲:數據庫操做日誌、用戶中心接口日誌、Drmc 接口日誌、Memcache 更新日誌等等。
按照嚴重級別分爲:trace、info、profile、warning、error。 咱們可能但願把不一樣業務邏輯(數據庫日誌、與其它系統交互的日誌)的日誌記錄到不一樣
的文件中,這樣能夠分門別類的查看。由於 error 日誌通常比較嚴重,因此咱們可能還但願 把全部的 error 記錄到一個單獨的文件中或者 mongodb 中。Yii 中的日誌路由組件能夠將不 同類別的日誌路由到不一樣的目的地(文件、數據庫、郵箱和頁面),利用它能夠很是方便維護 和管理日誌。
以下是一個日誌路由組件的配置,該配置將不一樣業務邏輯的日誌記錄到不一樣的文件中, 把錯誤日誌單獨記錄到 error.log 文件中,把嚴重的日誌直接發郵件,在開發過程還將日誌輸 出到頁面上,加快了開發速度。具體配置以下:
'log'=>array(
'class'=>'CLogRouter', 'routes'=>array(
array(//數據庫日誌記錄到db.log中 'class'=>'CFileLogRoute', 'categories'=>'db.*', 'logFile'=>'db.log',
),
array(//與用戶中心交互的日誌記錄到uc.log中 'class'=>'CFileLogRoute', 'categories'=>'uc.*',
'logFile'=>'uc.log',
),
array(//與Drmc交互的日誌記錄到uc.log中 'class'=>'CFileLogRoute', 'categories'=>'drmc.*', 'logFile'=>'drmc.log',
),
array(//全部的錯誤日誌記錄到error.log中 'class'=>'CFileLogRoute', 'levels'=>'error', 'logFile'=>'error.log',
),
array(//由於用戶中心很重要,全部的用戶中心錯誤日誌須要離開發郵件
'class'=>'CEmailLogRoute', 'categories'=>'uc.*', 'levels'=>'error', 'emails'=>'admaker@baidu.com',
),
array(//開發過程當中,把全部的日誌直接打印到頁面底部,這樣就不須要登陸服務器看日誌了
'class'=>'CWebLogRoute' 'levels'=>'trace,info,profile,warning,error',
),
)
經過上面的代碼能夠知道,Yii 的日誌記錄是經過配置數組驅動的,接下來對 Yii 中日誌
處理進行深刻的分析,下圖描述 Yii 中日誌處理的流程和原理:
一、根據日誌路由組件的配置, 生成多個日誌記錄對象
日誌路由組件
日誌記錄對象1
db.log
二、將日誌保存 到日誌緩衝區
日誌 緩衝區
三、緩衝區滿或 請求結束時通知 日誌路由組件
日誌記錄對象2
。。。
uc.log
日誌記錄對象i
五、把日誌 輸出到指定 的目的地
發送郵件日誌
四、每一個日誌記錄 對象從緩衝區中取 出本身須要的日誌
日誌記錄對象N
日誌輸出到頁面
一次 Http 請求的過程當中,記錄日誌的處理流程分以下 5 個階段:
Step1:根據日誌路由器的配置信息,生成各個日誌記錄對象,即 CFileLogRoute、
CEmailLogRoute 和 CWebLogRoute 的對象,日誌路由組件統一管理這些對象;
Step2:程序員調用寫日誌的接口(見下表),來記錄日誌,全部的日誌都是暫時保存在一 個 php 的數組緩衝區中;
Step3:當緩衝區滿的時候或請求處理結束的時候,會觸發以個 Flush 事件,通知日誌路 由組件來取日誌,這裏使用的就是是觀察者模式;
Step4:每一個日誌記錄對象分別取出本身須要的日誌,好比數據庫的日誌、用戶中心交 互日誌、error 級別的日誌等,即各取所需;
Step5:每一個日誌記錄對象分別保存本身的日誌。CFileLogRoute 對象把日誌保存到文件 中;CEmailLogRoute 對日誌進行發送郵件;CWebLogRoute 把日誌輸出到 web 頁面上。
Yii 提供了以下 4 個接口用於記錄日誌:
接口名稱 |
用途 |
Yii::log($msg,$level=CLogger::LEVEL_INFO,$category='application') |
記錄日誌 |
Yii::trace($msg,$category='application') |
記錄調試日誌 |
Yii::beginProfile($token,$category='application') |
記錄 profile 開始時刻 |
Yii::endProfile($token,$category='application') |
記錄 profile 結束時刻 |
3.二、Url 管理組件
url 管理組件主要提供 2 個功能:
一、根據用戶輸入的 url,解析出處理這個請求的路由——由哪一個 Controller 的哪一個 Action 來處理,同時將 url 中的部分參數添加到$_GET 參數中。在每一個 web 框架中都須要一個這樣 的組件來進行路由分發的工做。
二、根據路由和參數數組來建立 url。在視圖層能夠對 url 進行硬編碼,即直接寫死 url
地址,可是這每每缺少靈活性,爲後期的維護帶來成本。
array(
'components'=>array( 'urlFormat'=>'path', 'rules'=>array(
'/art/<cate:\w+>/<key:\d+>/<id:\d+>/<p:\d+>'=>'article/<cate>/<key>', 'post/<id:\d+>/<title:.*?>'=>'post/view', '<controller:\w+>/<action:\w+>'=>'<controller>/<action>',
),
),
);
如上是一個 url 管理組件的配置,一共有 3 條規則。下圖以第一條規則爲例,說明了 url 解析和 url 建立的 2 個功能。對於每一個路由規則,CUrlManager 都會建立一個 CUrlRule 對象 來處理這條規則對應的這個 2 個功能,因此說有一條規則就會有幾個 CUrlRule 對象。因此 CUrlRule 纔是 url 管理的核心所在,接下來分析 CUrlRule 的工做原理。
url:/art/apple/12/95/74
輸出
CUrlManager
輸入
解析url 輸出
路由爲:/art/apple/12
新增$_GET字段:
$_GET[id]=95,$_GET[p]=74
路由爲:/art/apple/12
輸出
新增$_GET字段: 輸出
$_GET[id]=95,$_GET[p]=74
CUrlManager
建立url
輸入 url:/art/apple/12/95/74
每條 url 路由規則由一個 CUrlRule 對象來進行處理,接下來以以下路由規則爲例: '/art/<cate:\w+>/<key:\d+>/<id:\d+>/<p:\d+>'=>'article/<cate>/<key>',說明 url 解析和 url 建立 的處理過程。每一個 CUrlRule 對象處理 url 的過程能夠分爲 3 個階段:
輸入:'/art/<cate:\w+>/<key:\d+>/<id:\d+>/<p:\d+>'=>'/article/<cate>/<key>' |
|||
|
|
||
|
初始化CUrlRule對象 |
|
構造函數中初始化以下成員變量: pattern='/^/art/(?P<cate>:\w+)/(?P<key>:\d+)/<?P<id>:\d+)/(?P<p>:\d+)/u'; routePattern='/article/(?P<cate:\w>/<?P<key:\d>/'; template='/art/cate/key/id/p';
route='article/<cate>/<key>';
references=array('cate'=>'<cate>','key'=>'<key>'); params=array('id'=>'\d+','p'=>'\d+');
輸入url:/art/apple/12/95/74
輸入
解析url
建立url
輸入route:/art/apple/12 輸入數組:array(id=>95,p=>74)
輸入
使用pattern對url進行匹配,匹配數組爲:
array( cate=>apple,key=>12,id=>95,p=>74)
使用routePattern對輸入route進行匹配,匹配數組爲:
array( cate=>apple,key=>12)
在references數組中
在params數組中
使用以下數組替換route array( cate=>apple,key=>12) 替換得route='article/apple/12'
將id和p添加到_GET中
$_GET[id]=95
$_GET[p]=74
使用合併後的數組對template進行替換,得到url爲
/art/apple/12/95/74
一、 初始化 CUrlRule 對象
在 CUrlRule 對象的構造函數中,會初始化 6 個重要的成員變量:
成員變量名稱 |
用途 |
pattern |
用於對 url 進行匹配的正則表達式,在解析 url 階段使用 |
routePattern |
用於對路由進行匹配的正則表達式,在建立 url 階段使用 |
template |
記錄 url 由哪些字段組成,是建立 url 的模板,在建立 url 階段, 是要將這些字段填上值,就能夠獲得須要的 url 了 |
route |
路由路徑的格式 |
references |
路由路徑中哪些字段來源與輸入的 url,在解析 url 階段使用 |
params |
url 中哪些字段須要添加到$_GET 數字中去,在解析 url 階段使用 |
二、 解析 url
解析 url 的工做分 3 步走:a、根據 pattern 規則,解析出 url 中的各個字段;b、根據 references
對路由中的引用字段進行替換;c、將 params 中指定的字段添加到$_GET 數組中
三、 建立 url
建立 url 的工做分 3 步走:a、根據 routePattern 規則,解析出輸入的路由中各個字段;b、 將輸入的參數數組和上一步解析的數組進行合併;c、用合併後的數組對 template 進行替換
3.三、異常處理組件
異常處理組件與 CApplication 一塊兒配合來處理全部異常(未捕獲的)。
CApplication
構造函數中安裝異 常處理句柄
異常處理句柄處理
handleException/handleError
。。。 執行流程
throw new exception
。。。
記錄日誌 句柄1 句柄2 句柄n
觸發onException/onError事件
事件句柄處理
否 獲取錯誤詳細描述
異常事件是否被處理
是
CErrorHandler
是否指定Action展現錯誤
否 是
調用end方法 觸發onEndRequest事件
使用本身的方 法顯示錯誤
建立Action顯 示錯誤
經過上圖能夠看出,CApplication 將它的 handleException/handleError 方法註冊爲事件處 理句柄,即 CApplication 獲得全部的異常,而後將它交給異常處理組件處理。
異常處理最主要的工做是給瀏覽器端展現異常信息,通常都是將異常交給某個 Action 來展現:若是是正常請求,就返回一個異常頁面;若是是 ajax 請求,就返回一個 json,由 瀏覽器端的 javascript 對 json 進行展現。
3.四、Cache 組件
使用緩存能夠很好的提高 web 系統的性能,經常使用的緩存有:memcache、apc 和 redis 等,
Yii 定義了 CCache 類,它爲訪問各類緩存設定了統一的接口。
接口名 |
用途 |
get() |
從緩存中讀一條數據 |
mget() |
從緩存中讀多條數據 |
set() |
往緩存中寫一條數據 |
add() |
往緩存中添加一條數據 |
delete() |
從緩存中刪除一條數據 |
flush() |
清空緩存 |
以下圖,Yii 使用 CCache 的子類來表示緩存組件,好比:CApcCache 表示 apc 緩存,
CMemCache 表示 memcache 緩存。
CApcCache
CDbCache
CEAcceleratorCache
CCache
CFileCache CMemCache
CWinCache
CXCache
CZendDataCache
默認狀況下,緩存數據的失效依賴於緩存設定的時間,可是緩存數據也能夠依賴於其它 條件而失效。咱們將一個依賴關係表現爲一個 CCacheDependency 或其子類的實例。當調 用 set()時,咱們連同要緩存的數據將其一同傳入。以下圖,Yii 內置了 5 種依賴關係類。
CDbCacheDependency
CDirectoryCacheDependency
CCacheDependency
CExpressionDependency
CFileCacheDependency
CGlobalStateCacheDependency
下面用一個例子講解緩存和緩存依賴的用法和原理。好比咱們有一個論壇,論壇首頁有 一個最新帖子區(顯示最新的 20 個帖子),即只要用戶發表帖子,那麼刷新首頁就能夠馬上 看到他發表的帖子,不能有延時。保存帖子的表名爲 Post,發帖時間爲 createTime 字段,如 下顯示了獲取最新帖子的主要代碼:
array(
'components'=>array( 'cache'=>array(
'class'=>'CMemCache',//配置緩存,使用memcache進行緩衝 'servers'=>array(array('host'=>'127.0.0.1', 'port'=>11211)),
),),
);
$top20Post = Yii::app()->cache->get('top20Post');//從cache中讀數據
if($top20Post==false){
$top20Post = Yii::app()->db->createCommand('select * from Post order by createTime desc limit 20')->queryAll();//從數據庫中查詢
$dependcy = new CDbCacheDependency('select max(createTime) from Post');//建立緩存依賴,依賴於最新發帖時間
Yii::app()->cache->set('top20Post', $top20Post, 600, $dependcy);//往cache中寫數據
}
從上面的代碼能夠看出,首先對 cache 配置,這裏使用的是 memcache,接着從 cache
中取數據,若是 cache 已經失效,則將從數據庫中獲取的數據從新保存到 cache 中,緩存依 賴使用的最新的發帖時間。
接下來分析一下寫 cache 和讀 cache 兩種操做的原理,緩存依賴其實就是在寫緩存的時 候獲取一下最新發帖時間,而後在讀緩存的時候再獲取一下最新發帖時間,判斷這 2 個時間 是否有變化,若是不相等,就能夠說明緩存失效了。
寫緩存處理流程
讀緩存處理流程
計算緩存依賴的值,即從數據庫查詢最 新的發帖時間,保存到 CDbCacheDependency對象中 |
|
|
|
把$top20Post和CDbCacheDependency這 2個對象進行序列化,一塊兒存入cache
驗證緩存依賴,執行CDbCacheDependency對象 的isChange方法:查詢最新的發帖時間,判斷 是否發生變化,若是變化則說明緩存應該失效
3.五、角訪問控制組件
基於角色的訪問控制(Role-Based Access Control)提供了一種簡單而又強大的集中訪問控 制機制,Yii 經過 CAuthManager 組件實現了分等級的 RBAC 機制。
在 Yii 的 RBAC 中,一個最基本的概念是「受權項目」(authorization item)。一個受權項 目就是作某件事的權限(例如新帖發佈,用戶管理)。根據其權限的粒度,受權項目可分爲 3 種類型:操做(operations)、任務(tasks)和角色(roles)。一個角色由若干任務組成,一個任務 由若干操做組成,而一個操做就是一個許可,不可再分,即角色的粒度最粗,操做的粒度最 細。例如,咱們有一個系統,它有一個管理員角色,它由帖子管理和用戶管理任務組成。用 戶管理任務能夠包含建立用戶,修改用戶和刪除用戶操做組成。爲保持靈活性,Yii 還容許
一個角色包含其餘角色或操做,一個任務能夠包含其餘操做,一個操做能夠包括其餘操做。
受權項目的名字必須是惟一的,一個受權項目可能與一個業務規則關聯(bizRule)。業務 規則是一段 PHP 代碼,在進行驗證受權項目的訪問權限檢查時,會被執行。僅在執行返回 爲 true 時,用戶纔會被視爲擁有此受權項目所表明的權限許可。例如,當定 義一個 updatePost(更新帖子)操做時,咱們能夠添加一個檢查當前用戶 ID 是否與此帖子的做者 ID 相 同的業務規則,這樣,只有做者本身才有更新帖子的權限。
經過受權項目,咱們能夠構建一個受權等級體系。在等級體系中,若是項目 A 由另外 的項目 B 組成(或者說 A 繼承了 B 所表明的權限),則 A 就是 B 的父項目。一個受權項目可 以有多個子項目,也能夠有多個父項目。所以,受權等級體系是一個偏序圖(partial-order graph) 結構而不是一種樹狀結構。在這種等級體系中,角色項目位於最頂層,操做項目位於最底層, 而任務項目位於二者之間。
一旦有了受權等級體系,咱們就能夠將此體系中的角色分配給用戶。而一個用戶一旦被 賦予一個角色,他就會擁有此角色所表明的權限。例如,若是咱們賦予一個用戶管理員的角 色,他就會擁有管理員的權限,包括帖子管理和用戶管理(以及相應的操做,例如建立用戶)。 定義受權等級體總共分三步:定義受權項目,創建受權項目之間的關係,還要分配角色
給用戶。authManager 應用組件提供了用於完成這三項任務的一系列 API。 Step1:要定義一個受權項目,可調用下列方法之一,具體取決於項目的類型:
CAuthManager::createRole CAuthManager::createTask CAuthManager::createOperation
創建受權項目以後,咱們就能夠調用下列方法創建受權項目之間的關係:
CAuthManager::addItemChild CAuthManager::removeItemChild CAuthItem::addChild CAuthItem::removeChild
最後,咱們調用下列方法將角色分配給用戶。
CAuthManager::assign CAuthManager::revoke
下面的代碼演示了使用 Yii 提供的 API 構建一個受權體系的例子:
$auth=Yii::app()->authManager;
$auth->createOperation('createPost','create a post');
$auth->createOperation('readPost','read a post');
$auth->createOperation('updatePost','update a post');
$auth->createOperation('deletePost','delete a post');
$bizRule='return Yii::app()->user->id==$params["post"]->authID;';
$task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule);
$task->addChild('updatePost');
$role=$auth->createRole('reader');
$role->addChild('readPost');
$role=$auth->createRole('author');
$role->addChild('reader');
$role->addChild('createPost');
$role->addChild('updateOwnPost');
$role=$auth->createRole('editor');
$role->addChild('reader');
$role->addChild('updatePost');
$role=$auth->createRole('admin');
$role->addChild('editor');
$role->addChild('author');
$role->addChild('deletePost');
$auth->assign('reader','readerA');
$auth->assign('author','authorB');
$auth->assign('editor','editorC');
$auth->assign('admin','adminD');
//檢查權限
checkAccess('deletePost');
一、創建權限樹
用 二、給用戶分配權限
戶 庫
權限樹 1
2 3
三、查詢是否擁有權限
4 5 6 7
8 9
基於角色的訪問控制可使用上圖來描述整個使用的過程,共分 3 步走
一、定義權限樹中的各個節點:操做(operations)、任務(tasks)和角色(roles),建立出它們 之間的包含關係,即建立出權限樹
二、給用戶分配權限,每一個用戶能夠擁有權限樹中的 0 個到對個節點所表明的權限
三、驗證用戶是否擁護每一個權限,爲了加快驗證速度,該組件對權限驗證作了優化。比 如用戶 Jerry 擁有權限節點 3 和 4,如今要驗證用戶是否擁有節點 9 所表明的權限。Yii 的權 限組件不是遍歷全部節點 3 和 4 的全部孩子節點來進行驗證的,而是遍歷全部節點有的父節 點來進行驗證,即只要遍歷節點 九、六、3 這三個節點就好了,這比遍歷節點 3 和 4 的全部孩 子節點來速度要快。
3.六、全局狀態組件
該組件 (CStatePersister) 用 於 存 儲 持 久 化 信 息 , 這 些 信 息 會 被 序 列 化 後 保 存 到
application.runtime.state.bin 文件中,該組件提供了 2 個接口:
接口名 |
用途 |
load() |
從硬盤(cache 組件)中讀取文件信息,反序列化 |
save() |
將信息進行序列化,而後保存爲硬盤文件 |
一、讀取全局狀態 四、修改全局狀態
getGlobalState getGlobalState
二、加載數據
load save
五、註冊onEndRequest事件
onEndRequest 六、請求處理結束時,
觸發onEndRequest事件
CApplication
句柄1 句柄2
CStatePersister
三、讀硬盤
七、寫硬盤
cache組件
硬盤文件
CApplication 使用該組件來進行持續存儲,經過上圖大體能夠描述它們之間的交互過程, 經過調用 app 的 getGlobalState 和 setGlobalState 能夠對全局狀態進行操做。第 2 步加載數據 的時候能夠從 cache 中加載,這是對硬盤文件的緩存,能夠加快 load 數據的速度。第 4 步 修改全局狀態,不會觸發馬上寫硬盤,全部的修改操做都是在 onEndRequest 事件觸發的時 候一塊兒寫硬盤的,即在請求快退出的時候寫硬盤,從而加快的保存的速度。
CStatePersister 經過硬盤文件對數據進行持久化,若是是多臺服務器就須要對硬盤文件 進行同步。因此若是是多臺服務器,應該將持久化的數據保存到一箇中心點上,好比數據庫, 對 CStatePersister 進行派生,實現數據庫的讀寫操做便可。
四、控制器層
4.一、Action
每一個 http 請求最後都是交給一個 Action 來處理,「Url 路由組件」會將請求路由到某個 控制器的某個 Action,每一個 Action 都有一個惟一的名字——ActionID。通常會把將業務邏輯 相近的 Action 放在同一個 Controller 中,它們通常使用相同的頁面佈局(layout)和模型層。有 2 種定義 Action 的方法:
一、外部 Action 對象:定義單獨的 Action 類,以 CAction 爲基類
二、內部 Action 對象:在 Controller 中定義以 action 打頭的成員函數
Yii 使用 CAction 爲這 2 種方式定義的 Action 封裝了一致的接口——即這 2 種方法定義的
Action 都存在一個 run 方法:
一、對於定義單獨的 Action 類——外部 Action,重寫父類的 run 方法便可
二、對於定義以 action 打頭的成員函數——內部 Action,Yii 使用 CInlineAction 作了一層 代理,CInlineAction 的 run 方法最後會調用的控制器的以 action 打頭的成員函數。
這樣的話,控制器的 Action 就所有對象化了,即每一個 Action 都是一個對象(內部 Action 對象和外部 Action 對象)。每一個控制器經過 actions 方法來制定使用那些外部 Action 對象,並 給它們分配惟一的 ActionID,因此外部 Action 能夠被多個 Controller 複用。下面以一個例子 來講明如何定義內部 Action 和外面 Action,以及調用時的處理流程:
class CCaptchaAction extends CAction{//驗證碼Action
public function run(){
...
$this->renderImage($this->getVerifyCode());//顯示驗證碼圖片
}
}
class SiteController extends CController{
public function actions(){//引用外部Action return array( 'captcha'=>'CCaptchaAction',//驗證碼Action
'about'=>'CViewAction' //線上靜態頁面的Action
);
}
public function actionPost($type, $page=1){//顯示帖子的Action
...
$this->render('post', ...);
}
public function actionComments($cate, $type){//顯示回帖的Action
...
$this->render('comments', ...);
}
}
SiteController 控制器一共定義了 4 個 Action,2 個內部 Action,2 個外部 Action。
使用 actions 方法就能夠方便的引入外部 Action,這爲 Action 的複用提供了很好的基礎。 可使用下圖來描述 SiteController 與 Action 之間的關係:
SiteController PostAction
經過actions()方法 引入外部Action
CommentsAction
外部Action,能夠 被多個控制器複用
CCaptchaAction CViewAction XXAction
經過actions()方法 引入外部Action
XXController
Ok,至此已經說明了控制器與 Action 之間的結構關係,接下來對 Action 對象的 run 方 法進行分析,即一個 http 請求過來控制器如何運行 Action 的 run 方法的,大部分處理邏輯 是在 CAction 的 runWithParams 和 runWithParamsInternal 兩個成員函數中,具體分析見下圖:
控制器執行某個Action
是否爲內部Action?
是 否
構造CInlineAction對象 構造CAction子類對象
內部Action對應的
從GET請求中分解出 有 參數賦值給成員函數
actionXXX成員函數是否 有參數?
外部Action的run方
法是否有參數?
有 從GET請求中分解出 參數賦值給run方法
參數沒問題
參數對不上,即不匹配
拋異常
無
運行Action成員函數
無
運行run方法
參數沒問題 參數對不上,即不匹配
拋異常
4.二、Filter
Filter(過濾器)與 Action 之間的關係能夠比喻成房子和圍牆的關係,執行某個 Action 則可 以比喻成一我的要去這個房子裏取東西,那麼他要作 3 個工做:一、翻入圍牆;二、進入房間 取東西;三、翻出圍牆。每一個 Action 能夠配置多個過濾器,那麼這我的就須要翻入和翻出多 道圍牆。
經過上面的比喻,就基本理解過濾器的用途和情景了。過濾器實際上是兩段代碼:一部分 代碼在 Action 以前執行;一部分代碼在 Action 以後執行。在控制器中的 filter()成員函數中配 置各個 Action 須要哪些過濾器——即配置過濾器列表,過濾器的執行順序就是它們出如今 過濾器列表中的順序。過濾器能夠阻止 Action 及後面其餘過濾器的執行,一個 Action 能夠 有多個過濾器。
下面舉一個例子,好比只容許北京的登陸用戶訪問「個人我的信息」頁面,同時須要記 錄這個頁面的訪問性能,即記錄這個 Action 的執行時間。對於這種需求,須要定義 2 個過
濾器:一、權限過濾器——用於驗證用戶訪問權限;二、性能過濾器——用於測量控制器執行 所用的時間。下圖說明的過濾器和 Action 執行的前後順序。
Step1 Step2
權限過濾器 性能過濾器
Action:MyInfo
Step3
Step4
驗證是否登陸 驗證ip是否合法
記錄Action運 行的開始時間
執行Action
記錄Action運行的結束時間 記錄該Action運行的總時間
有 2 中方法來定義過濾器,即內部過濾器和外部過濾器,接下來分別講解。 內部過濾器:定義爲一個控制器類的成員函數,方法名必須以 filter 開頭。下面定義一
個「ajax 過濾器」,即只容許 ajax 請求才能訪問的 Action——filterAjaxOnly(),那麼這個過濾 器的名稱就是 ajaxOnly,以下是這個過濾器的具體定義
public function filterAjaxOnly($filterChain){
if($_SERVER['HTTP_X_REQUESTED_WITH']==='XMLHttpRequest')
$filterChain->run();//調用$filterChain->run()讓後面的過濾器與Action繼續執行
else
}
throw new CHttpException(400,'這不是Ajax請求');//到此爲止,直接返回
外部過濾器:定義一個 CFilter 子類,實現 preFilter 和 postFilter 兩個成員函數,preFilter 成員函數中的代碼在 Action 以前執行,postFilter 成員函數中的代碼在 Action 以後執行。下 面定義一個「性能過濾器」,用於記錄相應 Action 執行使用的時間,具體代碼以下,
class PerformanceFilter extends CFilter{
private $_startTime;
protected function preFilter($filterChain){//Action執行以前運行的代碼
$this->_startTime=time(); //記錄Action執行的開始時間
return true; //若是返回false,那麼Action就不會被運行
}
protected function postFilter($filterChain){//Action執行以後運行的代碼
$spendTime=time()-$this->_startTime;//計算Action執行的總時間
echo "{$filterChain->action->id} spend $spendTime second\n";//輸出Action的執行時間
}
}
爲了統一處理內部過濾器和外部過濾器,Yii 使用 CInlineFilter 作了一層代理,
CInlineFilter 的 filter 方法會調用的控制器的以 filter 打頭的成員函數,即 filterAjaxOnly()。
Ok,如上講解了過濾器的用途和過濾器的定義,接下來說解:一、如何給各個 Action 配 置過濾器;二、在運行 Action 的過程當中,過濾器對象是如何被建立和運行的。
如何給各個 Action 配置過濾器呢?只要在控制器中配置一下過濾器列表便可,以下所 示,經過 filter()成員函數來配置過濾器列表。對於內部過濾器,使用字符串進行配置;對於 外部過濾器,使用數組進行配置。「+」表示這個過濾器只應用於加號後面的 Action;「-」表
示這個過濾器只應用於除減號後面的 Action 之外的全部 Action。
class PostController extends CController{
......
public function filters(){//配置過濾器列表
return array( 'ajaxOnly+delete,modify',//只有delete和modify兩個Action須要配置ajax過濾器 'postOnly+edit,create', //只有edit和create兩個Action須要配置post過濾器 array(
'PerformanceFilter-edit,create',//除了edit和create之外的全部Action都要統計執行時間 'unit'=>'second',//性能過濾器的共有屬性賦值
),
);
}
}
在運行 Action 的過程當中,過濾器對象是如何被建立和運行的呢?下圖就是答案。
當前運行的Action
Action1
一、輸入當前Action對象 和過濾器列表
過濾器鏈管理對象
Action2 Action3
Controller
二、查找過濾器列表 看看哪些過濾器須要應用於當前Action 建立這些過濾器對象,把它們放在一個鏈表中
過濾器鏈
filter()
執行Action1
過濾器列表
運行過濾器鏈
三、運行過濾器鏈表,分3步走 step一、運行filter一、filter二、filter3的preFilter方法中的代碼 step二、執行Action1 step三、運行filter一、filter二、filter3的postFilter方法中的代碼
過濾器鏈管理對象(CFilterChain)起了關鍵的做用:一、根據控制器提供的過濾器列表建立 出應用於當前 Action 的過濾器鏈,主要就是理解「+」和「-」;二、CFilterChain 提供了一個 run 成員函數,運行過濾器鏈的時候,run 函數會取出第一個過濾器,第一個過濾器執行完 preFilter 函數後,又會調用 CFilterChain 的 run 函數,此時 run 函數會取出第 2 個過濾器,等 等。具體就是上圖的調用流程,其實就是造成一個函數調用棧,比如是本節剛開始講的翻圍 牆的比喻。
4.三、Action 與 Filter 的執行流程
前兩節分表講了 Action 和 Filter 的定義與使用,它們是控制器的核心,下面把它們串起 來看一下,具體能夠用下圖描述:
建立Action的過程
Controller
actinonA1 actinonA2 actinonA3
missingAction
處理 否
根據actionId建立action對象 |
|
|
|
建立Action對象是否成功?
是 執行CWebApplication的
beforeControllerAction方法
二、controller->actions()所指的外部Action中找
三、controller->actions()所指的外部ActionProvider中找
Widget做爲 ActionProvider
外部Action
ActinonB1 ActinonB2 ActinonB3
外部Action
ActinonB1 ActinonB2 ActinonB3
actionId
controller->filters()
建立當前Action的過濾器鏈
輸入 CFilterChain::create 輸出
filter1
filter2 filter3 。。。
CFilterChain->run()
runWithParams
執行Action
執行CWebApplication的
afterControllerAction方法
主要的步驟能夠描述以下:
一、 根據控制器拿到的 ActionID 來建立 Action 對象,ActionID 由 Url 管理組件提供
二、 根據上一步建立的 Action 對象和控制器提供的過濾器列表,建立出應用於當前 Action
的全部過濾器對象,把這些對象放入一個鏈表
三、 用函數調用棧的方式執行各個過濾器對象和 Action;即執行順序爲:
filter1->preFilter()àfilter2->preFilter()àfilter3->preFilter()àAction->run()àfilter3->postFil ter()àfilter2->postFilter()àfilter1->postFilter()
4.四、訪問控制過濾器
訪問控制過濾器(CAccessControlFilter)是對控制器的各個 Action 的訪問權限進行控制,通 過控制器的 accessRules()成員函數能夠對各個 Action 的權限進行配置。CAccessControlFilter 支持以下幾個訪問權限的控制
一、 用戶是否登錄進行過濾,對用戶名進行過濾 另外:*任何用戶、?匿名用戶、@驗證經過的用戶
二、 對用戶所應有的角色進行過濾,即基於角色的權限控制
三、 對 ip 段進行過濾
四、 對請求的類型進行過濾,好比 GET、POST
五、 對 php 表達式進行過濾,好比 expression'=>'!$user->isGuest && $user->level==2'
好比對於 modifyAction,只有管理員角色的用戶才能訪問,而且 ip 段必須在 60.68.2.205
網段,該配置具體以下:
public function accessRules()
{
return array(
array('allow',
'actions'=>array('modify'), 'roles'=>array('admin'), 'ips'=>array('60.28.205.*'),
),
array('deny',
'actions'=>array('modify'), 'users'=>array('*'), 'message'=>'須要管理員身份才能訪問'
),
);
}
從控制器的accessRules中得到控制規則 |
|
|
|
建立CAccessControlFilter實 例,對於每一個驗證規則都建立 出一個CAccessRule實例
得到當前請求的信息: 一、request組件 二、user組件 三、請求類型(Get/Post) 四、請求ip
For循環每一個accessRule規則對象
驗證是否失敗?
否 發行
是
是否登陸
否 是
跳轉到登陸頁 面
拋異常 顯示出錯的message
上圖是 CAccessControlFilter 進行權限控制的處理流程圖,整個處理流程主要分爲 4 步:
Step1:首先建立出訪問控制過濾器對象,再根據控制器的 accessRules()成員函數提供的 規則,建立出一堆 accessRule 規則對象,即每條規則對應一個對象; Step2:獲取請求得到當前請求的信息,主要是以下 4 個信息
a、request 組件 b、user 組件 c、請求類型(Get/Post) d、請求的 ip
Step3:For 循環各個 accessRule 規則對象,即對每一個規則單獨驗證,只要有一個規則拒絕, 那麼後面的規則就不會再驗證。每一個規則都會驗證各類配置的每一個字段:用戶信息、角色、
ip、php 表達式等;
Step4:某一個 accessRule 規則對象驗證失敗,若是用戶沒有登陸則跳轉到登陸頁面, 若是已經登陸,則拋出異常,顯示驗證失敗規則所對應的 message 字段。
五、模型層
模型層用於數據的查詢和更新等操做,主要與數據庫打交道,Yii 的模型層能夠分爲 3
層。
一、DAO 層:對於數據庫的直接操做,Yii 使用 php 的 PDO,因此能夠支持大多數類型的
數據庫
二、元數據與 Command 構造器層:經過 CDbSchema 能夠得到各個表的結構信息,經過
Command 構造器直接構造出 Command 對象
三、ORM 層:使用 AvtiveRecord 技術實現了對象關係映射,這一層的實現依賴於上兩層 提供的功能
5.1 、DAO 層
Yii 的 DAO 層是對 PHP 的 PDO 層的簡單封裝,PDO 層提供了 2 個類:PDO 和 PDOStatement, Yii 的 DAO 層使用 4 個類對查詢的邏輯進行了分離:CDbConnection、CDbTransaction、 CDbCommand 和 CDbDataReader,它們的用途可使用下圖來描述:
表示一個數據庫鏈接
表示一次事務執行的
過程 表示一條sql語句
表示一條sql語句執 行返回的數據集
CDbConnection CDbTransaction CDbCommand CDbDataReader
PDO PDOStatement
5.1.一、數據庫鏈接組件
CDbConnection 表示一個數據庫的連接,通常將它定義爲框架的組件來使用。經過設置 它的成員屬性 active 能夠啓動數據庫連接,啓動連接主要作以下 2 個工做:
建立pdo對象
初始化數據庫連接: 一、設置字符集 二、執行initSQL
重要成員變量和成員函數有:
charset |
連接數據使用的字符集(SET NAMES utf8) |
createCommand() |
建立一個 Command 對象 |
beginTransaction() |
開始一個事務,新建一個事務對象 |
getCurrentTransaction() |
得到當前的事務對象 |
getSchema() |
得到元數據對象 |
getCommandBuilder() |
得到 CommandBuilder 對象 |
getPdoInstance() |
得到原生的 pdo 對象 |
getLastInsertID() |
得到剛插入列的自增主鍵 |
5.1.二、事務對象
CDbTransaction 表示一個事務的執行對象,通常是由 CDbConnection 的 beginTransaction
成員函數建立,生命週期爲一次事務的執行,重要成員變量和成員函數有:
commit() |
事務提交 |
rollback() |
事務回滾 |
5.1.3 、Command 對象
CDbCommand 表示一個命令的執行對象,通常是由 CDbConnection 的 createCommand 成員函數建立,生命週期爲一次命令的執行。全部的 sql 語句執行都是使用 prepare 模式, 這能夠提升反覆執行的 sql 語句效率,而對於大部分 web 系統,sql 語句通常是固定的而且 是屢次運行的,因此能夠提升模型層的執行速度。
下圖分析了內部查詢函數 queryInternal 的執行流程,CDbCommand 對於全部的 sql 都進 行 prepare 操做,主要是爲了提供類似 sql 的執行速度。對每次執行的 sql 語句所花費的時間 進行記錄。
根據enableParamLogging配置,對執行的sql記錄日誌 |
|
|
|
對sql執行prepare |
|
|
|
bind參數 |
|
|
|
得到查詢結果 |
|
|
|
根據enableProfiling配置,記錄查詢使用的時間
重要成員變量和成員函數有:
bindParam() |
對參數進行綁定 |
bindValue() |
對參數進行綁定 |
execute() |
執行一條 sql |
query() |
查詢,返回 CDbDataReader 對象 |
queryAll() |
查詢,返回全部行的數據 |
queryRow() |
查詢,返回一行的數據 |
queryScalar() |
查詢,返回第一列的數據 |
queryColumn() 查詢,返回第一行第一列的數據
CDbCommand 還提供了構建 sql 的成員函數:select(),from(),join(),where()等等,使 用這些函數就無需直接寫 sql 語句了,CDbCommand 使用 buildQuery()來構建出最終的 sql 語句。我的認爲這些成員函數的用處不大,由於已經很底層了,直接寫 sql 就能夠了,沒有 必要使用這些函數來構建 sql 語句。
5.2 、元數據與 Command 構造器
5.2.一、表結構查詢
CDbSchema 用於獲取各個表的元數據信息:表的各個字段、每一個字段的類型、主鍵信 息和外鍵信息等。
CDbSchema 爲 ORM 提供了表的結構信息,CDbSchema 使用 show create table、show columns、show tables 語句來得到表的元數據信息,最終將表結構存儲在 3 個類中:CDbSchema、 CDbTableSchema、CDbColumnSchema;這 3 個類是一種包含的關係:CDbSchema 表明一個 db , CDbSchema 包 含 多 個 table(CDbTableSchema) ,每一個 CDbTableSchema 又 包 含 多 個 column(CDbColumnSchema)。如下圖爲例,當前數據庫一共有 3 張表組成,每一個表又有 3 個 列組成:
CDbSchema
CDbColumnSchema1_1
CDbTableSchema1
CDbColumnSchema1_2
CDbColumnSchema1_3
CDbColumnSchema1_1
CDbTableSchema2
CDbColumnSchema1_2
CDbColumnSchema1_3
CDbColumnSchema1_1
CDbTableSchema3
CDbColumnSchema1_2
CDbColumnSchema1_3
下圖分析了 loadTable()函數的工做流程,該函數輸入是 table name,返回是這個表的結 構對象——即返回一個 CDbTableSchema 對象:
輸入表名 |
|
|
|
建立CDbTableSchema表對象 |
|
|
|
使用show columns語句查詢表的列結構 |
|
|
|
爲每一個列建立CDbColumnSchema列對象 |
|
|
|
分析show columns結果初始化列對象 |
|
|
|
經過show create table的結果分析出表的外鍵 |
|
|
|
返回CDbTableSchema表對象
5.2.二、查詢條件對象
CDbCriteria 表明一個查詢條件,用來是表示 sql 語句的各個組成部分:select、where、 order、group、having 等等,即便用一個對象來表明一條 sql 語句的查詢條件,從而使用該 對象就能夠設定數據庫查詢和操做的範圍。
CDbCriteria 提供了多個成員函數來對查詢條件進行設置和修改,多個查詢條件也能夠進 行合併。
重要成員變量和成員函數有:
select |
sql 語句中的 select 語法 |
condition |
sql 語句中的 where 語法 |
limit |
sql 語句中的 limit 語法 |
offset |
sql 語句中的 offset 語法 |
order |
sql 語句中的 order 語法 |
group |
sql 語句中的 group 語法 |
join |
sql 語句中的 join 語法 |
having |
sql 語句中的 having 語法 |
with |
查詢關聯對象 |
together |
是否拼接成一條 sql 執行,默認爲 true |
scopes |
查詢使用的名字空間 |
addCondition() |
添加一個查詢條件 |
addInCondition() |
添加 in 語法的查詢條件 |
addColumnCondition() |
添加多列匹配的查詢條件 |
compare() |
添加比較大小的查詢條件 |
addBetweenCondition() |
添加 between 查詢條件 |
mergeWith() |
與其它查詢對象進行合併 |
5.2.1 、Command 構造器
CDbCommandBuilder 主要有 2 個用途:建立 CDbCommand 對象;建立 CDbCriteria 對象。 建立 CDbCommand 對象功能:輸入爲表結構對象(CDbTableSchema)、查詢條件對象
(CDbCriteria)和數據庫須要更新的數據,CDbCommand 根據這三個輸入拼接出知足須要的 sql, 最後建立並返回一個 CDbCommand 對象。
表結構對象(CDbTableSchema)
查詢條件對象(CDbCriteria)
Input
CDbCommandBuilder
Output
能夠執行的
CDbCommand對象
insert/update語句使用的數據
建立 CDbCriteria 對象: 輸 入 爲 表 結 構 對 象 (CDbTableSchema) 和 查 詢 使 用 的 數 據 ,
CDbCommand 根據這兩個輸入拼接出知足須要的查詢條件對象或者查詢條件語句。
表結構對象(CDbTableSchema) 查詢使用的數據
Input CDbCommandBuilder
Output
一、查詢條件對象(CDbCriteria) 二、查詢條件語句
CDbCommandBuilder 爲 ActiveRecord 層提供了很好的支持。
5.三、ORM(ActiveRecord)
5.3.一、表的元數據信息
CActiveRelation 表示 ER 圖中表與表之間的關係,在 Yii 的 AR 模型中經過 CActiveRelation
對象來找到關聯對象。Yii 定義了 5 種關聯關係:
CBelongsToRelation |
N:1 關係 |
CHasOneRelation |
1:1 關係 |
CHasManyRelation |
1:N 關係 |
CManyManyRelation |
N:M 關係 |
CStatRelation |
統計關係 |
CActiveRecordMetaData 表示一個表的元數據信息,它有 2 部分組成:當前表的元數據 信息(CDbTableSchema)、與其它表的關聯關係元數據(CActiveRelation)。當對當前單表進行更 新和操做的時候使用當前表的元數據信息,當對當前表以及關聯表進行聯合查詢的時候使用 關聯關係元數據(CActiveRelation)。
5.3.2 、單表 ORM
Yii 使用 ActiveRecord 來實現 ORM,一個 CActiveRecord 子類(簡稱 AR 類)表明一張表,表 的列在 AR 類中體現爲類的屬性,一個 AR 實例則表示表中的一行。 常見的 CRUD 操做做爲 AR 的 方法實現。所以,能夠以一種更加面向對象的方式訪問數據。
每一個 AR 類都存在一個靜態對象,好比 經過 Post::model()能夠取到該靜態對象,該靜態對象主 要用於存儲表的「元數據」,以下圖,靜態對象在首次建立的時候會初始化元數據,該元數據會被 AR 類的全部實例使用,即在內存中只存在一份元數據對象(CActiveRecordMetaData)。
Post(帖子表)靜態對象 |
|
|
|
元數據
Post表元數據
元數據
。。。
BelongsTo User關聯關係元數據 HasMany Comment關聯關係元數據
元數據
。。。
接下來分析對於單表的更新和查找的處理流程,對於關聯表的查詢會在 CActiveFinder
中進行分析。
使用驗證器進行驗證 |
|
|
|
使用CDbCommandBuilder建立 insert/update Command |
|
|
|
執行Command |
|
|
|
更新當前對象
上圖 save 成員函數的保存流程。首先對要保存的各個字段進行合法性驗證,接着經過 CommandBuilder 建立相應的 Command,最後執行。能夠看出,關鍵是 Command 的合成主 要的工做由 CommandBuilder 完成的。
根據輸入條件,初始化criteria對象
調用成員函數applyScopes對「命名範圍」進行合併
criteria對象的with屬性是否爲空?
是 否,須要查詢關聯對象 使用CDbCommandBuilder
建立Find Command
CActiveFinder進行查詢
執行Command返回結果 返回結果
上圖分析了 find 系列成員函數的查詢流程。第一步將輸入條件進行合成爲一個 criteria
查詢對象;第二步對「命名範圍」進行合併,從而造成一個新的查詢對象;接下來兵分兩路,
若是須要查詢關聯對象,則經過 CActiveFinder 進行查詢,若是隻是單表查詢 則 經過
CommandBuilder 來建立相應的查詢 Command。 「命名範圍」表示當前表中記錄的一個子集,它最初想法來源於 Ruby on Rails。一個 Post
帖子表爲例,咱們能夠定義 2 個命名範圍:一、經過審覈的帖子,即 status=1;二、最新發表的帖子, 即按最近發表的 5 個帖子。若是將整個 Post 表當作一個集合的話,那麼前 2 個命名範圍就是它的子 集;以下圖:
published
子集
recently
子集
Post
集合
public function scopes(){
return array(
'published'=>array('condition'=>'status=1',), 'recently'=>array('order'=>'create_time DESC', 'limit'=>5,),
);
}
$posts=Post::model()->published()->recently()->findAll();//查找 Post::model()->published()->recently()->delete/update();//刪除或者更新
使用 scopes 成員函數來定義「命名範圍」,如上圖。在進行查詢、刪除、更新以前能夠 使用命名範圍來設定範圍,多個命名範圍能夠連接使用,接下分析命名範圍的工做原理。
調用不存在的成員函數,觸發 call()的調用 |
|
|
|
經過scopes()拿到全部的命名範圍 |
|
|
|
選擇指定的命名範圍 |
|
|
|
將選擇命名範圍與靜態對象當前的criteria進行merge操做
上圖分析了命名範圍的工做原理,全部的命名範圍最終會與靜態對象當前的 criteria 進行 合併,從而造成一個更加嚴格的查詢條件。在進行查詢或者更新操做的是時候,經過調用成 員函數 applyScopes 來得到命名範圍合併後的查詢條件 criteria。
5.3.3 、多表 ORM
多表 ORM 查詢的算法複雜度是 Yii 框架中最大的(不要怕,用多表 ORM 的時候只要知道大概 的流程就行),由於多表的 ORM 映射自己就很複雜,多表 ORM 須要考慮 5 種 ActiveRelation, 其次爲了優化查詢速度,在表作聯合的時候還須要考慮使用哪一個外鍵可使得查詢速度更快。
下圖是多表 ORM 查詢的一個處理流程,須要使用 4 個類:CActiveFinder、CJoinElement、
CStatElement、CJoinQuery。 CActiveFinder:查詢的發起者,它負責建立查詢樹和查詢查詢樹 CJoinElement:查詢樹有多個 CJoinElement 和 CStatElement 組成,給樹的每一個結點表示
一個表結構,由於 CStatElement 表示統計信息,因此它只能做爲葉子結點
CJoinQuery:對樹中多個結點構建聯合查詢,即構建 join 語句。執行聯合查詢語句,將 查詢的結果返回給 CJoinElement 的根結點
CActiveFinder
一、建立tree 二、調用find()
CJoinElement Post
五、執行查詢,得到數據
三、建立query對象
八、執行查詢,得到數據
CJoinQuery
四、添加查詢條件
CJoinElement User
九、執行query
得到數據
CStatElement
CJoinElement Comment
CJoinElement Category
七、添加查詢條件
CJoinQuery
Post Count
整個查詢共分 9 個步驟:
六、建立query對象
Step1:首先由 CActiveFinder 構建查詢結點樹,根據模型層提供的 relation()成員函數提 供的表之間關係構成查詢樹,能夠經過 with 語法指定多久關聯關係。樹根表明的是當前查 詢的表結構,樹中其它結點表示的是與樹根相關聯的其它表。
Step2:CActiveFinder 向查詢樹發起 find 操做,查詢樹的樹根會先找出是以一對多關係 的直接孩子結點(這樣能夠有效利用索引),先對這些孩子進行聯合查詢
Step3:建立出一個 CJoinQuery 對象,該對象用於查詢,將樹根的查詢條件添加到
CJoinQuery 對象中
Step4:遍歷 Step2 中的一對多關係的孩子,把他們的查詢條件添加到 CJoinQuery 對象 中,次數 CJoinQuery 已經得到的全部的查詢條件
Step5:對上 2 步添加進 CJoinQuery 對象的節點構建聯合查詢語句,執行這條語句,將 執行的結果保存到樹根節點中
Step6:遍歷樹根剩餘的直接孩子,剩下的孩子就是多對一或者多對多的關係了。此時 建立一個新的 CJoinQuery 對象
Step7:將當前節點的查詢條件和樹根節點的查詢添加到 CJoinQuery 對象中
Step8:構建 Step7 添加的查詢條件,對着 2 個節點構建聯合查詢語句,執行這條語句, 將執行的結果保存到樹根節點中
Step9:最後還剩下一個統計關係節點,由於樹根節點所對應的表數據已經所有查詢出 來,因此 CStatElement 能夠直接從樹根節點中得到數據,構建 group by 的 sql 語句來直接執 行,就無需使用 CJoinQuery 了,執行構建 group by 語句,將執行的結果保存到樹根節點中 Ok,經過如上 9 步查詢能夠得到 ORM 關聯表的所有信息,其中前 5 步用於查詢一對多 的關聯表,Step6 到 Step8 用於查詢多對一和多對多關係表,Step9 則一步用於查詢統計關係
表。
5.3.四、CModel 與 CValidator
驗證器用於對 CModel 的各個屬性進行驗證,從而保證存儲數據的合法性。咱們以回帖 表 Comment 的模型層爲例,它的驗證規則定義以下:
public function rules(){
return array(
array('content, author, email', 'required'),
//帖子內容、做者、郵箱不能爲空
array('author, email, url', 'length', 'max'=>128),
//做者、郵箱、URL的長度最大爲128
array('email','email'),
//郵箱必須合法
array('url','url'),
//url地址必須合法
);
}
CModel 的 validate()成員函數的出來流程爲:
Comment
Validator list
Required:content,author,email
選擇適用於當前情景的驗證器
分別運行每一個驗證器,每一個 驗證器又能夠驗證多個屬性
content author email url
Length:author,email,url
Email:email Url:url
六、視圖層
6.一、視圖渲染流程
在 MVC 架構中,View 主要是用於展現信息的。Yii 中的視圖層文件由 2 部分組成:佈局 視圖、部分視圖。web 系統的大部分頁面都存在相同的元素:logo、菜單、foot 欄等,咱們 把這些相同的元素組成的視圖文件稱爲佈局視圖,通常 web 系統須要 2 個佈局,即前臺布 局和後臺佈局,前臺佈局是給用戶看的,後臺佈局是給管理員看的。每一個頁面所獨有的部分 視圖稱爲部分視圖
菜單欄
菜單欄
導
航 部分視圖1 欄
導
航 部分視圖2 欄
Footer欄
Footer欄
可使用上圖進行描述,咱們將菜單欄、導航欄和 Footer 欄放到佈局文件中,即全部 頁面複用一個佈局文件,而後每一個頁面(Action)有各自的部分視圖文件。
接下來看一下視圖文件的存放路徑。WebApp 能夠配置視圖文件路徑和佈局文件路徑同 時還會指定一個默認的佈局文件;每一個 Controller 的視圖文件存放在 WebApp 指定的視圖路 徑下,以 Controller 的名字職位後綴,Controller 還能夠指定本身使用哪一個佈局文件。
WebApp 成員屬性 |
說明 |
viewPath |
用於指定視圖文件路徑,全部的視圖文件必須在這個文件下 默認 protected/views |
layoutPath |
用於指定佈局文件路徑,全部的佈局文件必須在這個文件下 默認 protected/views/layouts,該路徑下有:main.php、column.php |
viewPath |
用於指定系統視圖文件路徑,默認 protected/views/system |
layout |
指定默認使用的佈局文件,默認爲 main |
好比當前正在執行 PostController 的 modifyAction,PostController 指定使用 column 佈局,那 麼 這 個 請 求 所 使 用 的 布 局 文 件 爲 protected/views/layouts/column.php , 視 圖 文 件 爲 protected/views/post/modify.php。
視圖層中還有 2 個重要的概念:客戶端腳本組件、Widget。 客戶端腳本組件:該組件用於管理客戶端腳本(javascript 和 css),能夠經過該組件向視
圖中添加 javascript 和 css,客戶端腳本組件統一管理這些代碼,在頁面輸出的最後一步對客 戶端腳本(javascript 和 css)進行渲染。
Widget:又稱小物件,經過 Widget 能夠對頁面進行模塊化,Widget 能夠當作是一個沒 有佈局的控制器。經過 Widget 能夠把公用的頁面元素進行復用,好比:Menu Widget、列表
Widget、表格 Widget、分頁 Widget 等等。
二、Layout render
渲染布局視圖
日曆Widget 菜單Widget
客 戶
註冊 端 腳
js/css 本
組 件
三、渲染出最終的 html
把註冊的js/css插入到指 定的位置
視圖層的渲染分 3 個步驟完成: Step1:渲染部分視圖,即渲染每一個頁面各自特有的視圖片段; Step2:將渲染布局視圖,即即渲染每一個頁面共有的頁面元素,同時將 Step1 的結果插入到 佈局視圖中。在 Step1 和 Step2 中,可能還須要渲染 Widget,好比日曆 Widget、菜單 Widget 等。這 2 個步驟中能夠註冊本身使用了哪些 js 和 css;
Step3:渲染 js 和 css。將前 2 步註冊的 js 和 css 添加到 html 頁面的制定位置。
6.二、Widget
在 windows(MFC,Delphi,遊戲)開發過程當中,有不少小控件(下拉菜單/按鈕/日曆/人物)可 以使用,不須要從頭開發。小物件( Cwidget) 的設計思想與其相似,主要是能夠爲了提高視 圖層的開發速度,它將頁面當作是有多個能夠複用的控件組成,從而提升了頁面控件的複用 性和可維護性。
下面說一個日曆組件的例子,日曆組件的輸入是一個數組,以下圖,輸出分 2 部分:html 片段;向 clientScript 註冊這個日曆須要使用的 js 和 css
以下是兩種建立 Widget 的方法:
<?php $this->beginWidget('path.to.WidgetClass'); ?>
...可能會由小物件獲取的內容主體...
<?php $this->endWidget(); ?> 或者
<?php $this->widget('path.to.WidgetClass'); ?>
Yii 自帶了 20 個左右的經常使用 widget,開源社區目前也貢獻了 100 多個 widget。小物件可 以配置多套皮膚(國慶用紅色的,清明用灰色的)。
6.三、客戶端腳本組件
大部分頁面須要使用 js 和 css,通常狀況下咱們是直接在頁面中引用 js 文件或者直接寫 js 代碼。客戶端腳本組件(CClientScript)用於管理視圖層的 JavaScript 和 CSS 腳本,能夠根據 方便的管理和維護 js 和 css:
一、 註冊 js、css 文件
經過 ClientScript 組件的提供的註冊接口來向 html 代碼中註冊 js 和 css 文件,這樣的話 不用直接修改模板文件,爲 widget 的開發提供了很好的基礎——視圖層模塊化。
二、 註冊 js、css 代碼
咱們但願在 dom ready 的時候,對某些 dom 綁定時候;那麼須要在 html 頁面最下面或 者在某個 js 文件中寫這個代碼。經過 ClientScript 組件的提供的註冊接口來向 html 代碼中注 冊 js 代碼,目前能夠知道這個代碼嵌入到什麼地方(domready/onload)。
三、 解決 Js、css 代碼依賴、代碼合併、代碼壓縮、代碼版本控制
好比有 2000 個頁面中使用的 edit.js 這個文件,而它又使用了 jquery.cookie.js,那麼這 2000 個頁面須要同時引用這 2 個 js,若是有一天 edit.js 使用了 jquery.tab.js 中的功能,那麼須要 在這 2000 個頁面中都增長對 jquery.tab.js 的引用,老大,2000 個啊!對於代碼合併、代碼 壓縮、代碼加版本號都是是相似的,一旦發生修改,須要修改大量的模板頁面。Yii 經過 ClientScript 組件的包依賴數組和 scriptMap 數組就能夠解決這些問題,簡單來講就是隻要改 一下這 2 個 php 的數組便可。
Ok,下面咱們先看一下 ClientScript 組件的重要的成員屬性和成員函數,主要歸爲 3 類: 初始化成員屬性、腳本保存成員屬性、註冊接口。初始化成員屬性用於配置 package 和腳本 映射;腳本保存成員屬性用於記錄註冊了哪些腳本,這些腳本分別註冊到什麼位置;註冊接 口用於外部調用,將腳本提交給 ClientScript 組件。
初始化 成員屬性 |
$packages |
保存用戶腳本(js/css)包和包依賴關係 |
$corePackages |
保存框架腳本包和包依賴關係 |
|
$scriptMap |
對腳本進行映射,從而實現腳本的壓縮、 合併、版本控制 |
|
保存腳本的 成員屬性 |
$css |
保存 css 代碼片段 |
$cssFiles |
保存 css 文件的 url |
|
$scripts |
保存 js 代碼片段 |
|
$scriptFiles |
保存 js 文件的 url |
|
$coreScripts |
保存使用了哪些 package |
|
註冊接口 |
registerCss($id,$css,$media) |
註冊 css 代碼 |
registerCssFile($url,$media) |
註冊 css 文件 |
|
registerScript($id,$script,$position) |
註冊 js 代碼 |
|
registerScriptFile($url,$position) |
註冊 js 文件 |
|
registerPackage($name) |
註冊 package |
|
registerCoreScript($name) |
註冊 package |
ClientScript 組件在使用的過程當中分三步走:對組件進行配置、調用註冊接口註冊腳本、 渲染插入腳本。可使用下圖來形象的描述,接下來詳細分析每一步的工做。
三、渲染頁面時,把註冊的 腳步插入到指定的位置
ClientScript組件 的配置信息
ClientScript組件 的註冊接口
一、定義組件的
$package和
$scriptMap
二、經過註冊接 口註冊js和css
CClientScript的 重要成員變量
$packages
$corePackages
$scriptMap
$css
$cssFiles
$scripts
$scriptFiles
$coreScripts
對coreScript中的packages進 行解包,把js和css加入到
$cssFiles和$scriptFiles中
根據$scriptMap映射對 $cssFiles和$scriptFiles 進行替換 |
|
|
|
對$cssFiles和$scriptFiles 進行去重
把$css、$scripts、
$cssFiles$scriptFiles中的腳步插入 到html文檔的合適的位置
Step一、對 ClientScript 組件進行配置,下面是該組件的配置數組
'clientScript'=>array( 'class'=>'CClientScript', 'packages'=>array(//用戶的js、css代碼包結構
'edit'=>array(
'js'=>array('edit.js'), 'depends'=>array('yiitab'),
),
'favor'=>array(
'js'=>array('favor.js'), 'depends'=>array('jquery'),
)
),
'corePackages'=>array(//框架的js、css代碼包結構 'jquery'=>array(
'js'=>array(YII_DEBUG ? 'jquery.js' : 'jquery.min.js'),
),
'yii'=>array(
'js'=>array('jquery.yii.js'), 'depends'=>array('jquery'),
),
'yiitab'=>array( 'js'=>array('jquery.yiitab.js'), 'depends'=>array('jquery'),
)
...
),
'scriptMap'=>array(//對js代碼進行映射 'edit.js'=>'edit.js?version=1.0',//代碼加版本號 'favor.js'=>'favor.min.js',//代碼壓縮 '*.js'=>'common.min.js?version=1.1',//代碼合併、壓縮、加版本號
)
)
如上配置數組定義了 packages 和 corePackage 進行賦值,經過 scriptMap 能夠對 js 和 css
進行壓縮、合併、加版本信息
Step二、調用註冊接口,註冊腳本
//註冊favor package
Yii::app()->clientScript->registerPackage('favor');
//將edit.js註冊到頁面的最下部
Yii::app()->clientScript->registerScriptFile('edit.js', CClientScript::POS_END);
//在頁面onload的時候執行一段js代碼
Yii::app()->clientScript->registerScript(
'id_load',
'alert("the page is load!")', CClientScript::POS_LOAD
);
//在dom ready的時候執行一段js代碼(dom ready事件要早於onload事件)
Yii::app()->clientScript->registerScript(
'id_ready',
'alert("dom is ready!")', CClientScript::POS_READY
);
javascript 能夠註冊到 html 中的 5 個位置,具體見下表
註冊的位置 |
說明 |
POS_HEAD |
把 js 文件或代碼註冊到<title>標籤以前 |
POS_BEGIN |
把 js 文件或代碼註冊到<body>標籤以後 |
POS_END |
把 js 文件或代碼註冊到</body>標籤以前 |
POS_LOAD |
在觸發 onload 事件時,執行執行註冊的 js 代碼 |
POS_READY |
在 dom ready 的時候,執行執行註冊的 js 代碼 |
Step三、渲染頁面時,將註冊的 js、css 插入到指定的位置。從註冊的 package 中得到 js 和 css; 對 js 和 css 進行 map 映射,最後在輸出的 html 文檔中進行正則匹配,嵌入 js 和 css 代碼, 如上兩步最後輸出的 html 文檔爲:
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="language" content="en" />
<script type="text/javascript" src="/common.min.js?version=1.1"></script>
<script type="text/javascript" src="/favor.min.js"></script>
<title>ClientScript組件學習</title>
</head>
<body>
<div>
....
</div>
registerPackage('favor')
registerScriptFile('edit.js',ClientScript::POS_END)
<script type="text/javascript" src="'edit.js?version=1.0"></script>
<script type="text/javascript">
/*<![CDATA[*/
jQuery(function($) { alert("dom is ready!")
});
registerScript('id_ready','alert("dom is ready!")',CClientScript::POS_READY)
jQuery(window).load(function() { alert("the page is load!")
});
/*]]>*/
</script>
</body>
registerScript('id_load','alert("the page is load!")',CClientScript::POS_LOAD)