首页 > 代码库 > YII框架源码分析(百度PHP大牛创作-原版-无广告无水印)

YII框架源码分析(百度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‘=>‘admaker@baidu.com‘,

),

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="http://www.mamicode.com/common.min.js?version=1.1"></script>

<script type="text/javascript" src="http://www.mamicode.com/favor.min.js"></script>

<title>ClientScript组件学习</title>

 

</head>

<body>

<div>

....

</div>

 

registerPackage(‘favor‘)

 

 

 

registerScriptFile(‘edit.js‘,ClientScript::POS_END)

 

<script type="text/javascript" src="http://www.mamicode.com/‘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)