程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 網頁編程 >> PHP編程 >> 關於PHP編程 >> YII框架源碼分析(百度PHP大牛創作-原版-無廣告無水印)

YII框架源碼分析(百度PHP大牛創作-原版-無廣告無水印)

編輯:關於PHP編程

 

 

 

 

 

 

 

 

 

 

 

 

YII 框架源碼分析

 

 

 

 

 

 

百度聯盟事業部——黃銀鋒

 

目 錄

1、 引言 3

1.1、Yii 簡介 3

1.2、本文內容與結構 3

2、組件化與模塊化 4

2.1、框架加載和運行流程 4

2.2、YiiBase 靜態類 5

2.3、組件 6

2.4、模塊 9

2.5 、App 應用   10

2.6 、WebApp 應用   11

3、系統組件 13

3.1、日志路由組件  13

3.2、Url 管理組件  15

3.3、異常處理組件  17

3.4、Cache 組件   17

3.5、角訪問控制組件  19

3.6、全局狀態組件  21

4、控制器層 23

4.1 、Action  23

4.2 、Filter   24

4.3 、Action 與 Filter 的執行流程   26

4.4、訪問控制過濾器  27

5、模型層 30

5.1 、DAO 層   30

5.1.1、數據庫連接組件   30

5.1.2、事務對象   31

5.1.3 、Command 對象   31

5.2 、元數據與 Command 構造器   32

5.2.1、表結構查詢   32

5.2.2、查詢條件對象   33

5.2.1 、Command 構造器   33

5.3 、ORM(ActiveRecord)   34

5.3.1、表的元數據信息   34

5.3.2 、單表 ORM   34

5.3.3 、多表 ORM   36

5.3.4 、CModel 與 CValidator   37

6、視圖層 38

6.1、視圖渲染流程  38

6.2、Widget   39

6.3、客戶端腳本組件  40

 

1、引言

 

1.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.2、本文內容與結構

 

本文對 Yii1.1.8 版本的源代碼進行了深入的分析,本文的內容與結構為: 組件化與模塊化:對 Yii 的基於組件和事件驅動編程模式的基礎類(CComponent)進行分

析;對組件化和模塊化的工作原理進行分析;對 WebApp 應用創建 Controller 流程等進行分 析。

系統組件:對 Yii 框架自帶的重要組件進行分析,主要包括:日志路由組件、Url 管理組 件、異常處理組件、Cache 組件、基於角色的訪問控制組件等。

控制器層:控制器主要包含 Action 和 Filter,對 Action 與 Filter 的工作原理進行分析。 模型層:對 DAO 層、元數據和 Command 構造器、ORM 的原理進行分析

視圖層:對視圖層的渲染過程、Widget 和客戶端腳本組件等進行分析

 

本文檔中的錯誤或不妥之處在所難免,殷切希望本文檔的讀者給予批評指正!

 

2、組件化與模塊化

 

2.1、框架加載和運行流程

 

 

 

start

 

 

加載YiiBase.php 1、安裝autoload方法, 為類的實例化做准備 2、獲得框架所有類的路 徑(Yii1.1.8共208類)

 

 

 

 

根據ActionId創建Action對象 1、從成員函數中尋找Action 2、從ActionMap中尋找Action

 

 

加載request組件

(Get,Post,Cookie)

創建Action 否 異 是否成功? 常

 

 

創建WebApp實例 1、初始化別名:application、 webroot、ext 2、安裝異常處理句柄,拋異常時 交給異常處理組件來處理 3、配置核心組件:urlManager、 errorHandler、session、db等 4、根據配置信息來配置組件、子 模塊、WebApp成員屬性等 5、加載preload組件:log組件、 request組件等

 

 

 

運行WebApp

1、觸發onBeginRequest事件

 

 

加載Url管理組件 根據配置信息分析url,解析出路 由:route=ControllerId/ActionId

 

 

根據ControllerId創建控制器 1、從ControllerMap中尋找 2、從子模塊中尋找 3、從ControllerPath中尋找

 

 

創建控制器 拋 是否成功?  否 異

 

 

根據filters()配置,創建出當 前Action的所有Filter對象

 

 

運行Filter1的preFilter方法 運行Filter2的preFilter方法

 

檢查Get參 拋 數與Action 否 異 的參數是否      常

一致

 

 

運行Action

 

 

 

 

Partial render

渲染出核心部分的html

 

 

 

Layout render

渲染出整體的html

 

 

Client Script render 嵌入javascript、css生 成最終的html

 

 

 

echo html

 

 

 

2、處理請求

 

 

運行控制器

 

運行Filter2的postFilter方法 運行Filter1的postFilter方法

 

3、觸發onEndRequest事件

 

 

 

注冊的句柄:

 

異常處理 組件

 

 

XX處拋異常

throw Exception。。。

 

end

比如記錄日志

 

 

Yii 框架加載和運行流程共分 4 個階段(也許看著有點嚇人,木有關系,我們先知道一個大概):

Step1:WebApp 初始化與運行

1.1、 加載 YiiBase.php,安裝 autoload 方法;加載用戶的配置文件;

1.2、 創建 WebApp 應用,並對 App 進行初始化,加載部分組件,最後執行 WebApp

Step2:控制器初始化與運行

2.1、 加載 request 組件,加載 Url 管理組件,獲得路由信息 route=ControllerId/ActionId 2.2、 創建出控制器實例,並運行控制器

Step3:控制器初始化與運行

3.1、 根據路由創建出 Action

3.2、 根據配置,創建出該 Action 的 Filter; 3.3、 執行 Filter 和 Action

Step4:渲染階段

4.1、 渲染部分視圖和渲染布局視圖

4.2、 渲染注冊的 javascript 和 css

 

 

 

2.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創建對象

 

 

1、如果類不存在,則通過import導入

通過new創建對象

 

2、new這個對象

 

3、根據輸入對這個對象的屬性初始化

 

 

 

 

 

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 中方法創建對象:1、使用 new 關鍵字;2、

 

使用 Yii::createComponent 方法。

當使用 new 關鍵字創建對象時,autoload 會分 3 步來尋找對應類的定義:a、判斷是否 為 framework 中的類(framework 的所有類和這個類的全路徑都保存在 YiiBase 的一個成員變 量中,就相當於整個框架都 import 了);2、判斷是否使用 Yii::import 導入了這個類,對於非 框架的類,我們在創建這個類的對象時需要先 import 這個類的定義;3、從 include_path 目 錄下查找以這個類名字命名的 php 腳本,所以在開發的時候類名盡量與文件名保存一致, 這樣我們導入(import)包含這個文件的文件夾就行了,從而無需把這個文件夾中的每個文件 都導入一遍。

當使用 Yii::createComponent 方法創建對象時,它提供了比 new 關鍵字更多的功能:a、 通過這個類的全路徑別名來指定類的位置和類名(類名必須與文件名一致),當這個類還沒有 導入的時候,會根據全路徑來自動導入這個類的定義;2、對創建出來的對象的成員變量進 行賦值。即如下圖描述,原來要寫 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.3、組件

 

CComponent 類就是組件,它為整個框架的組件編程和事件驅動編程提供了基礎,YII 框架中的大部分類都將 CComponent 類作為基類。CComponent 類為它的子類提供 3 個特性: 1、成員變量擴展

通過定義兩個成員函數(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 成員函數,你是 在調用一段代碼!

2、事件模型

事件模型就是設計模式中的“觀察者模式”:當對象的狀態發生了變化,那麼這個對象 可以將該事件通知其它對象。

為了使用事件模型,需要實現這三個步驟:1、定義事件;2、注冊事件句柄;3、觸發 事件。

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 是事件處理函 數鏈表。

3、行為類綁定

有兩種辦法可以對類添加特性:1、直接修改這個類的代碼:添加一些成員函數和成員 變量;2、派生:通過子類來擴展。很明顯第二種方法更加易維護、易擴展。如果需要對一 個類添加多個特性(多人在不同時期),那麼需要進行多級派生,這顯然加大了維護成本。 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 類的特性:

1、 更好的配置一個對象,當設置對象的成員變量的時候,其實是運行一段代碼;

2、 更好的監聽一個對象,當對象的內部狀態發生變化的時候,其它對象可以得到通知;

3、 更好的擴展一個對象,可以給一個對象增加成員變量和成員函數,還能監聽這個對 象的狀態。

 

2.4、模塊

 

模塊是整個系統中一些相對獨立的程序單元,完成一個相對獨立的軟件功能。比如 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 方面的擴展:1、增加一個 run 方法;2、添加了若干成員屬性;3、添加了若干 組件。

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 步組成:

1、在成員變量 controllerMap 中查找,判斷是否有對應的 Controller,controllerMap 的 優先級最高

2、在子模塊中中查找,判斷是否有對應的 Controller 3、在 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、系統組件

 

3.1、日志路由組件

 

每個 Web 系統在運行的過程中都需要記錄日志,日志可以記錄到文件或數據庫中,在 開發階段可以把日志直接輸出到頁面得底部,這樣可以加快開發速度。Yii 在日志處理上做 了如下 2 點重要工作:

1、每個 Http 請求,可能需要記錄多條日志(數據庫更新日志/與其它系統交互日志)。比 如某次 Http 請求要記錄 18 條日志,我們是每記一條日志都寫一次硬盤(即寫 18 硬盤)呢,還 是在請求快結束的時候一次性寫硬盤?很顯然,先把這些日志保存在一個 php 的數組中, 在請求快結束的時候,把數組中的所有日志一次性寫硬盤速度要快一些。

2、每條日志可以從 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'=>'[email protected]',

),

array(//開發過程中,把所有的日志直接打印到頁面底部,這樣就不需要登錄服務器看日志了

'class'=>'CWebLogRoute' 'levels'=>'trace,info,profile,warning,error',

),

)

通過上面的代碼可以知道,Yii 的日志記錄是通過配置數組驅動的,接下來對 Yii 中日志

處理進行深入的分析,下圖描述 Yii 中日志處理的流程和原理:

 

 

 

1、根據日志路由組件的配置, 生成多個日志記錄對象

 

 

 

日志路由組件

 

 

日志記錄對象1

db.log

 

 

 

 

 

 

 

2、將日志保存 到日志緩沖區

 

 

日志 緩沖區

 

 

 

3、緩沖區滿或 請求結束時通知 日志路由組件

日志記錄對象2

 

 

 

。。。

uc.log

 

 

 

 

日志記錄對象i

 

5、把日志 輸出到指定 的目的地

 

 

 

發送郵件日志

 

 

4、每個日志記錄 對象從緩沖區中取 出自己需要的日志

 

 

 

日志記錄對象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.2、Url 管理組件

 

url 管理組件主要提供 2 個功能:

1、根據用戶輸入的 url,解析出處理這個請求的路由——由哪個 Controller 的哪個 Action 來處理,同時將 url 中的部分參數添加到$_GET 參數中。在每個 web 框架中都需要一個這樣 的組件來進行路由分發的工作。

2、根據路由和參數數組來創建 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

 

 

 

 

1、 初始化 CUrlRule 對象

在 CUrlRule 對象的構造函數中,會初始化 6 個重要的成員變量:

成員變量名稱

用途

pattern

用於對 url 進行匹配的正則表達式,在解析 url 階段使用

routePattern

用於對路由進行匹配的正則表達式,在創建 url 階段使用

template

記錄 url 由哪些字段組成,是創建 url 的模板,在創建 url 階段,

是要將這些字段填上值,就可以得到需要的 url 了

route

路由路徑的格式

references

路由路徑中哪些字段來源與輸入的 url,在解析 url 階段使用

params

url 中哪些字段需要添加到$_GET 數字中去,在解析 url 階段使用

2、 解析 url

 

解析 url 的工作分 3 步走:a、根據 pattern 規則,解析出 url 中的各個字段;b、根據 references

對路由中的引用字段進行替換;c、將 params 中指定的字段添加到$_GET 數組中

3、 創建 url

創建 url 的工作分 3 步走:a、根據 routePattern 規則,解析出輸入的路由中各個字段;b、 將輸入的參數數組和上一步解析的數組進行合並;c、用合並後的數組對 template 進行替換

 

 

 

3.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.4、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.5、角訪問控制組件

 

基於角色的訪問控制(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、給用戶分配權限

戶 庫

權限樹 1

 

2 3

 

 

 

 

 

3、查詢是否擁有權限

4 5 6 7

 

 

8 9

 

 

 

 

基於角色的訪問控制可以使用上圖來描述整個使用的過程,共分 3 步走

1、定義權限樹中的各個節點:操作(operations)、任務(tasks)和角色(roles),創建出它們 之間的包含關系,即創建出權限樹

2、給用戶分配權限,每個用戶可以擁有權限樹中的 0 個到對個節點所代表的權限

3、驗證用戶是否擁護每個權限,為了加快驗證速度,該組件對權限驗證做了優化。比 如用戶 Jerry 擁有權限節點 3 和 4,現在要驗證用戶是否擁有節點 9 所代表的權限。Yii 的權 限組件不是遍歷所有節點 3 和 4 的所有孩子節點來進行驗證的,而是遍歷所有節點有的父節 點來進行驗證,即只要遍歷節點 9、6、3 這三個節點就行了,這比遍歷節點 3 和 4 的所有孩 子節點來速度要快。

 

 

 

3.6、全局狀態組件

 

該組件 (CStatePersister) 用 於 存 儲 持 久 化 信 息 , 這 些 信 息 會 被 序 列 化 後 保 存 到

application.runtime.state.bin 文件中,該組件提供了 2 個接口:

接口名

用途

load()

從硬盤(cache 組件)中讀取文件信息,反序列化

save()

將信息進行序列化,然後保存為硬盤文件

 

 

 

1、讀取全局狀態 4、修改全局狀態

 

 

getGlobalState getGlobalState

 

2、加載數據

 

 

load save

 

 

5、注冊onEndRequest事件

onEndRequest 6、請求處理結束時,

觸發onEndRequest事件

 

 

 

 

CApplication

句柄1 句柄2

 

 

 

CStatePersister

 

3、讀硬盤

7、寫硬盤

 

cache組件

硬盤文件

 

 

CApplication 使用該組件來進行持續存儲,通過上圖大致可以描述它們之間的交互過程, 通過調用 app 的 getGlobalState 和 setGlobalState 可以對全局狀態進行操作。第 2 步加載數據 的時候可以從 cache 中加載,這是對硬盤文件的緩存,可以加快 load 數據的速度。第 4 步 修改全局狀態,不會觸發立刻寫硬盤,所有的修改操作都是在 onEndRequest 事件觸發的時 候一起寫硬盤的,即在請求快退出的時候寫硬盤,從而加快的保存的速度。

CStatePersister 通過硬盤文件對數據進行持久化,如果是多台服務器就需要對硬盤文件 進行同步。所以如果是多台服務器,應該將持久化的數據保存到一個中心點上,比如數據庫, 對 CStatePersister 進行派生,實現數據庫的讀寫操作即可。

 

4、控制器層

 

4.1、Action

 

每個 http 請求最後都是交給一個 Action 來處理,“Url 路由組件”會將請求路由到某個 控制器的某個 Action,每個 Action 都有一個唯一的名字——ActionID。一般會把將業務邏輯 相近的 Action 放在同一個 Controller 中,它們一般使用相同的頁面布局(layout)和模型層。有 2 種定義 Action 的方法:

1、外部 Action 對象:定義單獨的 Action 類,以 CAction 為基類

2、內部 Action 對象:在 Controller 中定義以 action 打頭的成員函數

Yii 使用 CAction 為這 2 種方式定義的 Action 封裝了一致的接口——即這 2 種方法定義的

Action 都存在一個 run 方法:

1、對於定義單獨的 Action 類——外部 Action,重寫父類的 run 方法即可

2、對於定義以 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.2、Filter

 

Filter(過濾器)與 Action 之間的關系可以比喻成房子和圍牆的關系,執行某個 Action 則可 以比喻成一個人要去這個房子裡取東西,那麼他要做 3 個工作:1、翻入圍牆;2、進入房間 取東西;3、翻出圍牆。每個 Action 可以配置多個過濾器,那麼這個人就需要翻入和翻出多 道圍牆。

通過上面的比喻,就基本理解過濾器的用途和情景了。過濾器其實是兩段代碼:一部分 代碼在 Action 之前執行;一部分代碼在 Action 之後執行。在控制器中的 filter()成員函數中配 置各個 Action 需要哪些過濾器——即配置過濾器列表,過濾器的執行順序就是它們出現在 過濾器列表中的順序。過濾器可以阻止 Action 及後面其他過濾器的執行,一個 Action 可以 有多個過濾器。

下面舉一個例子,比如只允許北京的登錄用戶訪問“我的個人信息”頁面,同時需要記 錄這個頁面的訪問性能,即記錄這個 Action 的執行時間。對於這種需求,需要定義 2 個過

 

濾器:1、權限過濾器——用於驗證用戶訪問權限;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,如上講解了過濾器的用途和過濾器的定義,接下來講解:1、如何給各個 Action 配 置過濾器;2、在運行 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

1、輸入當前Action對象 和過濾器列表

過濾器鏈管理對象

 

 

Action2 Action3

Controller

2、查找過濾器列表 看看哪些過濾器需要應用於當前Action 創建這些過濾器對象,把它們放在一個鏈表中

 

 

 

過濾器鏈

 

 

 

filter()

執行Action1

 

 

 

過濾器列表

 

 

 

 

運行過濾器鏈

 

 

 

3、運行過濾器鏈表,分3步走 step1、運行filter1、filter2、filter3的preFilter方法中的代碼 step2、執行Action1 step3、運行filter1、filter2、filter3的postFilter方法中的代碼

 

 

過濾器鏈管理對象(CFilterChain)起了關鍵的作用:1、根據控制器提供的過濾器列表創建 出應用於當前 Action 的過濾器鏈,主要就是理解“+”和“-”;2、CFilterChain 提供了一個 run 成員函數,運行過濾器鏈的時候,run 函數會取出第一個過濾器,第一個過濾器執行完 preFilter 函數後,又會調用 CFilterChain 的 run 函數,此時 run 函數會取出第 2 個過濾器,等 等。具體就是上圖的調用流程,其實就是形成一個函數調用棧,好比是本節剛開始講的翻圍 牆的比喻。

 

 

 

4.3、Action 與 Filter 的執行流程

 

前兩節分表講了 Action 和 Filter 的定義與使用,它們是控制器的核心,下面把它們串起 來看一下,具體可以用下圖描述:

 

 

創建Action的過程

Controller

actinonA1 actinonA2 actinonA3

 

 

 

 

 

 

 

 

missingAction

處理 否

 

 

根據actionId創建action對象

 

 

創建Action對象是否成功?

是 執行CWebApplication的

beforeControllerAction方法

 

 

 

2、controller->actions()所指的外部Action中找

 

 

 

3、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方法

 

主要的步驟可以描述如下:

1、 根據控制器拿到的 ActionID 來創建 Action 對象,ActionID 由 Url 管理組件提供

2、 根據上一步創建的 Action 對象和控制器提供的過濾器列表,創建出應用於當前 Action

的所有過濾器對象,把這些對象放入一個鏈表

3、 用函數調用棧的方式執行各個過濾器對象和 Action;即執行順序為:

filter1->preFilter()àfilter2->preFilter()àfilter3->preFilter()àAction->run()àfilter3->postFil ter()àfilter2->postFilter()àfilter1->postFilter()

 

 

 

4.4、訪問控制過濾器

 

訪問控制過濾器(CAccessControlFilter)是對控制器的各個 Action 的訪問權限進行控制,通 過控制器的 accessRules()成員函數可以對各個 Action 的權限進行配置。CAccessControlFilter 支持如下幾個訪問權限的控制

1、 用戶是否登陸進行過濾,對用戶名進行過濾 另外:*任何用戶、?匿名用戶、@驗證通過的用戶

2、 對用戶所應有的角色進行過濾,即基於角色的權限控制

3、 對 ip 段進行過濾

4、 對請求的類型進行過濾,比如 GET、POST

5、 對 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實例

 

獲得當前請求的信息: 1、request組件 2、user組件 3、請求類型(Get/Post) 4、請求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 字段。

 

5、模型層

 

 

模型層用於數據的查詢和更新等操作,主要與數據庫打交道,Yii  的模型層可以分為 3

層。

1、DAO 層:對於數據庫的直接操作,Yii 使用 php 的 PDO,所以可以支持大多數類型的

數據庫

2、元數據與 Command 構造器層:通過 CDbSchema 可以獲得各個表的結構信息,通過

Command 構造器直接構造出 Command 對象

3、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.1、數據庫連接組件

 

CDbConnection 表示一個數據庫的鏈接,一般將它定義為框架的組件來使用。通過設置 它的成員屬性 active 可以啟動數據庫鏈接,啟動鏈接主要做如下 2 個工作:

創建pdo對象

 

 

初始化數據庫鏈接: 1、設置字符集 2、執行initSQL

 

重要成員變量和成員函數有:

charset

鏈接數據使用的字符集(SET NAMES utf8)

createCommand()

創建一個 Command 對象

beginTransaction()

開始一個事務,新建一個事務對象

 

 

getCurrentTransaction()

獲得當前的事務對象

getSchema()

獲得元數據對象

getCommandBuilder()

獲得 CommandBuilder 對象

getPdoInstance()

獲得原生的 pdo 對象

getLastInsertID()

獲得剛插入列的自增主鍵

 

 

 

5.1.2、事務對象

 

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.1、表結構查詢

 

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.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

 

1、查詢條件對象(CDbCriteria) 2、查詢條件語句

 

CDbCommandBuilder 為 ActiveRecord 層提供了很好的支持。

 

 

 

5.3、ORM(ActiveRecord)

 

5.3.1、表的元數據信息

 

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 個命名范圍:1、通過審核的帖子,即 status=1;2、最新發表的帖子, 即按最近發表的 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

 

 

 

1、創建tree 2、調用find()

 

 

 

CJoinElement Post

5、執行查詢,獲得數據

 

 

 

3、創建query對象

 

8、執行查詢,獲得數據

 

 

CJoinQuery

 

4、添加查詢條件

 

 

CJoinElement User

9、執行query

獲得數據

CStatElement

CJoinElement Comment

CJoinElement Category

 

7、添加查詢條件

 

 

 

 

 

 

CJoinQuery

 

Post Count

 

整個查詢共分 9 個步驟:

6、創建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.4、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、視圖層

 

6.1、視圖渲染流程

 

在 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 等等。

 

 

 

 

 

 

 

 

2、Layout render

渲染布局視圖

 

 

 

 

日歷Widget 菜單Widget

客 戶

注冊 端 腳

js/css   本

組 件

 

 

 

 

3、渲染出最終的 html

把注冊的js/css插入到指 定的位置

 

 

 

視圖層的渲染分 3 個步驟完成: Step1:渲染部分視圖,即渲染每個頁面各自特有的視圖片斷; Step2:將渲染布局視圖,即即渲染每個頁面共有的頁面元素,同時將 Step1 的結果插入到 布局視圖中。在 Step1 和 Step2 中,可能還需要渲染 Widget,比如日歷 Widget、菜單 Widget 等。這 2 個步驟中可以注冊自己使用了哪些 js 和 css;

Step3:渲染 js 和 css。將前 2 步注冊的 js 和 css 添加到 html 頁面的制定位置。

 

 

 

6.2、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.3、客戶端腳本組件

 

大部分頁面需要使用 js 和 css,一般情況下我們是直接在頁面中引用 js 文件或者直接寫 js 代碼。客戶端腳本組件(CClientScript)用於管理視圖層的 JavaScript 和 CSS 腳本,可以根據 方便的管理和維護 js 和 css:

1、 注冊 js、css 文件

通過 ClientScript 組件的提供的注冊接口來向 html 代碼中注冊 js 和 css 文件,這樣的話 不用直接修改模板文件,為 widget 的開發提供了很好的基礎——視圖層模塊化。

2、 注冊 js、css 代碼

我們希望在 dom ready 的時候,對某些 dom 綁定時候;那麼需要在 html 頁面最下面或 者在某個 js 文件中寫這個代碼。通過 ClientScript 組件的提供的注冊接口來向 html 代碼中注 冊 js 代碼,目前可以知道這個代碼嵌入到什麼地方(domready/onload)。

3、 解決 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 組件在使用的過程中分三步走:對組件進行配置、調用注冊接口注冊腳本、 渲染插入腳本。可以使用下圖來形象的描述,接下來詳細分析每一步的工作。

3、渲染頁面時,把注冊的 腳步插入到指定的位置

 

 

 

 

 

 

 

 

 

ClientScript組件 的配置信息

 

 

 

 

 

 

 

ClientScript組件 的注冊接口

 

 

 

 

 

 

 

1、定義組件的

$package和

$scriptMap

 

 

 

 

 

 

2、通過注冊接 口注冊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文檔的合適的位置

 

 

 

 

Step1、對 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

進行壓縮、合並、加版本信息

 

 

Step2、調用注冊接口,注冊腳本

//注冊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 代碼

 

Step3、渲染頁面時,將注冊的 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)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved