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

 

 

 

 YII 框架源碼分析javascript

  

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

 

一、引言

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)

相關文章
相關標籤/搜索