Yii PHP 框架分析

作者:wdy 基于yii1.0.8

一. 框架运行流程

1. 启动

网站的唯一入口程序 index.php :

$yii=dirname(__FILE__).'/../framework/yii.php';
$config=dirname(__FILE__).'/protected/config/main.php';
 
// remove the following line when in production mode
defined('YII_DEBUG') or define('YII_DEBUG',true);
 
require_once($yii);
Yii::createWebApplication($config)->run();

上面的require_once($yii) 引用出了后面要用到的全局类Yii,Yii类是YiiBase类的完全继承:

class Yii extends YiiBase
{
}

系统的全局访问都是通过Yii类(即YiiBase类)来实现的,Yii类的成员和方法都是static类型。

2. 类加载

Yii利用PHP5提供的spl库来完成类的自动加载。在YiiBase.php 文件结尾处

spl_autoload_register(array('YiiBase','autoload'));

将YiiBase类的静态方法autoload 注册为类加载器。 PHP autoload 的简单原理就是执行 new 创建对象或通过类名访问静态成员时,系统将类名传递给被注册的类加载器函数,类加载器函数根据类名自行找到对应的类文件并include 。

下面是YiiBase类的autoload方法:

public static function autoload($className)
{
    // use include so that the error PHP file may appear
    if(isset(self::$_coreClasses[$className]))
        include(YII_PATH.self::$_coreClasses[$className]);
    else if(isset(self::$_classes[$className]))
        include(self::$_classes[$className]);
    else
        include($className.'.php');
}

可以看到YiiBase的静态成员$_coreClasses 数组里预先存放着Yii系统自身用到的类对应的文件路径:

private static $_coreClasses=array(
    'CApplication' => '/base/CApplication.php',
    'CBehavior' => '/base/CBehavior.php',
    'CComponent' => '/base/CComponent.php',
    // ...
)

非 coreClasse 的类注册在YiiBase的$_classes 数组中:

private static $_classes=array();

其他的类需要用Yii::import()讲类路径导入PHP include paths 中,直接

include($className.'.php')

3. CWebApplication的创建

回到前面的程序入口的 Yii::createWebApplication($config)->run();

public static function createWebApplication($config=null)
{
    return new CWebApplication($config);
}

现在autoload机制开始工作了。 当系统 执行 new CWebApplication() 的时候,会自动

include(YII_PATH.'/base/CApplication.php')

将main.php里的配置信息数组$config传递给CWebApplication创建出对象,并执行对象的run() 方法启动框架。

CWebApplication类的继承关系

CWebApplication -> CApplication -> CModule -> CComponent

$config先被传递给CApplication的构造函数

public function __construct($config=null)
{
    Yii::setApplication($this);
 
    // set basePath at early as possible to avoid trouble
    if(is_string($config))
        $config=require($config);
    if(isset($config['basePath']))
    {
        $this->setBasePath($config['basePath']);
        unset($config['basePath']);
    }
    else
        $this->setBasePath('protected');
    Yii::setPathOfAlias('application',$this->getBasePath());
    Yii::setPathOfAlias('webroot',dirname($_SERVER['SCRIPT_FILENAME']));
 
    $this->preinit();
 
    $this->initSystemHandlers();
    $this->registerCoreComponents();
 
    $this->configure($config);
    $this->attachBehaviors($this->behaviors);
    $this->preloadComponents();
 
    $this->init();
}

Yii::setApplication($this); 将自身的实例对象赋给Yii的静态成员$_app,以后可以通过 Yii::app() 来取得。 后面一段是设置CApplication 对象的_basePath ,指向 proteced 目录。

Yii::setPathOfAlias('application',$this->getBasePath());
Yii::setPathOfAlias('webroot',dirname($_SERVER['SCRIPT_FILENAME']));

设置了两个系统路径别名 application 和 webroot,后面再import的时候可以用别名来代替实际的完整路径。别名配置存放在YiiBase的 $_aliases 数组中。

$this->preinit();

预初始化。preinit()是在 CModule 类里定义的,没有任何动作。

$this->initSystemHandlers() 方法内容:

/**
 * Initializes the class autoloader and error handlers.
 */
protected function initSystemHandlers()
{
    if(YII_ENABLE_EXCEPTION_HANDLER)
        set_exception_handler(array($this,'handleException'));
    if(YII_ENABLE_ERROR_HANDLER)
        set_error_handler(array($this,'handleError'),error_reporting());  
}

设置系统exception_handler和 error_handler,指向对象自身提供的两个方法。

4. 注册核心组件

$this->registerCoreComponents(); 代码如下:

protected function registerCoreComponents()
{
    parent::registerCoreComponents();
 
    $components=array(
        'urlManager'=>array(
            'class'=>'CUrlManager',
        ),
        'request'=>array(
            'class'=>'CHttpRequest',
        ),
        'session'=>array(
            'class'=>'CHttpSession',
        ),
        'assetManager'=>array(
            'class'=>'CAssetManager',
        ),
        'user'=>array(
            'class'=>'CWebUser',
        ),
        'themeManager'=>array(
            'class'=>'CThemeManager',
        ),
        'authManager'=>array(
            'class'=>'CPhpAuthManager',
        ),
        'clientScript'=>array(
            'class'=>'CClientScript',
        ),
    );
 
    $this->setComponents($components);
}

注册了几个系统组件(Components)。 Components 是在 CModule 里定义和管理的,主要包括两个数组

private $_components=array();
private $_componentConfig=array();

每个 Component 都是 IApplicationComponent接口的实例,Componemt的实例存放在$_components 数组里,相关的配置信息存放在$_componentConfig数组里。配置信息包括Component 的类名和属性设置。

CWebApplication 对象注册了以下几个Component:urlManager, request,session,assetManager,user,themeManager,authManager,clientScript。 CWebApplication的parent 注册了以下几个 Component:coreMessages,db,messages,errorHandler,securityManager,statePersister。

Component 在YiiPHP里是个非常重要的东西,它的特征是可以通过 CModule 的 __get() 和 __set() 方法来访问。 Component 注册的时候并不会创建对象实例,而是在程序里被第一次访问到的时候,由CModule 来负责(实际上就是 Yii::app())创建。

5. 处理 $config 配置

继续, $this->configure($config); configure() 还是在CModule 里:

public function configure($config)
{
    if(is_array($config))
    {
        foreach($config as $key=>$value)
            $this->$key=$value;
    }
}

实际上是把$config数组里的每一项传给 CModule 的 父类 CComponent __set() 方法。

public function __set($name,$value)
{
    $setter='set'.$name;
    if(method_exists($this,$setter))
        $this->$setter($value);
    else if(strncasecmp($name,'on',2)===0
            && method_exists($this,$name))
    {
        //duplicating getEventHandlers() here for performance
        $name=strtolower($name);
        if(!isset($this->_e[$name]))
            $this->_e[$name]=new CList;
        $this->_e[$name]->add($value);
    }
    else if(method_exists($this,'get'.$name))
        throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.',
                             array('{class}'=>get_class($this), '{property}'=>$name)));
    else
        throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
                             array('{class}'=>get_class($this), '{property}'=>$name)));
    }
}

我们来看看:

if(method_exists($this,$setter))

根据这个条件,$config 数组里的basePath, params, modules, import, components 都被传递给相应的 setBasePath(), setParams() 等方法里进行处理。

6、$config 之 import

其中 import 被传递给 CModule 的 setImport:

public function setImport($aliases)
{
    foreach($aliases as $alias)
        Yii::import($alias);
}

Yii::import($alias)里的处理:

public static function import($alias,$forceInclude=false)
{
    // 先判断$alias是否存在于YiiBase::$_imports[] 中,已存在的直接return, 避免重复import。
    if(isset(self::$_imports[$alias])) // previously imported
        return self::$_imports[$alias];
 
    // $alias类已定义,记入$_imports[],直接返回
    if(class_exists($alias,false))
        return self::$_imports[$alias]=$alias;
 
    // 类似 urlManager 这样的已定义于$_coreClasses[]的类,或不含.的直接类名,记入$_imports[],直接返回
    if(isset(self::$_coreClasses[$alias]) || ($pos=strrpos($alias,'.'))===false) // a simple class name
    {
        self::$_imports[$alias]=$alias;
        if($forceInclude)
        {
            if(isset(self::$_coreClasses[$alias])) // a core class
                require(YII_PATH.self::$_coreClasses[$alias]);
            else
                require($alias.'.php');
        }
        return $alias;
    }
 
    // 产生一个变量 $className,为$alias最后一个.后面的部分
    // 这样的:'x.y.ClassNamer'
    // $className不等于 '*', 并且ClassNamer类已定义的,      ClassNamer' 记入 $_imports[],直接返回
    if(($className=(string)substr($alias,$pos+1))!=='*' && class_exists($className,false))
        return self::$_imports[$alias]=$className;
 
    // 取得 $alias 里真实的路径部分并且路径有效
    if(($path=self::getPathOfAlias($alias))!==false)
    {
        // $className!=='*',$className 记入 $_imports[]
        if($className!=='*')
        {
            self::$_imports[$alias]=$className;
            if($forceInclude)
                require($path.'.php');
            else
                self::$_classes[$className]=$path.'.php';
            return $className;
        }
        // $alias是'system.web.*'这样的已*结尾的路径,将路径加到include_path中
        else // a directory
        {
            set_include_path(get_include_path().PATH_SEPARATOR.$path);
            return self::$_imports[$alias]=$path;
        }
    }
    else
        throw new CException(Yii::t('yii','Alias "{alias}" is invalid. Make sure it points to an existing directory or file.',
                             array('{alias}'=>$alias)));
}

7. $config 之 components

$config 数组里的 $components 被传递给CModule 的setComponents($components)

public function setComponents($components)
{
    foreach($components as $id=>$component)
    {
        if($component instanceof IApplicationComponent)
            $this->setComponent($id,$component);
        else if(isset($this->_componentConfig[$id]))
            $this->_componentConfig[$id]=CMap::mergeArray($this->_componentConfig[$id],$component);
        else
            $this->_componentConfig[$id]=$component;
    }
}

$componen是IApplicationComponen的实例的时候,直接赋值: $this->setComponent($id,$component),

public function setComponent($id,$component)
{
    $this->_components[$id]=$component;
    if(!$component->getIsInitialized())
        $component->init();
}

如果$id已存在于_componentConfig[]中(前面注册的coreComponent),将$component 属性加进入。 其他的component将component属性存入_componentConfig[]中。

8. $config 之 params

这个很简单

public function setParams($value)
{
    $params=$this->getParams();
    foreach($value as $k=>$v)
        $params->add($k,$v);
}

configure 完毕!

9. attachBehaviors

$this->attachBehaviors($this->behaviors);

空的,没动作

预创建组件对象 $this->preloadComponents();

protected function preloadComponents()
{
    foreach($this->preload as $id)
        $this->getComponent($id);
}

getComponent() 判断_components[] 数组里是否有 $id的实例,如果没有,就根据_componentConfig[$id]里的配置来创建组件对象,调用组件的init()方法,然后存入_components[$id]中。

10. init()

$this->init();

函数内:$this->getRequest(); 创建了Reques 组件并初始化。

11. run()

public function run()
{
    $this->onBeginRequest(new CEvent($this));
    $this->processRequest();
    $this->onEndRequest(new CEvent($this));
}

二. Yii 组件分析

Yii是基于组件(component-based)的web框架,CComponent类是所有组件的基类。

CComponent类为子类提供了基于属性(property)、事件(event)、行为(behavior)编程接口。

组件的属性(property)

Ccomponent类并没有提供属性的变量存储,需要由子类来提供两个方法来实现。子类的getPropertyName()方法提供$component->PropertyName的取值操作数据,子类的setPropertyName($val)方法提供$component->PropertyName赋值操作。

$width=$component->textWidth;     // 获取 textWidth 属性

实现方式为调用子类提供的方法 $width=$component->getTextWidth()

$component->textWidth=$width;     // 设置 textWidth 属性

实现方式为调用子类提供的方法 $component->setTextWidth($width)

public function getTextWidth()
{
    return $this->_textWidth;
}
 
public function setTextWidth($value)
{
    $this->_textWidth=$value;
}

组件的属性值是大小写不敏感的(类的成员时大小写敏感的)

组件的事件(event)

组件事件是一种特殊的属性,它可以将事件处理句柄(可以是函数名、类方法或对象方法)注册(绑定)到一个事件名上,句柄在事件被唤起的时候被自动调用。 组件事件存放在CComponent 的$_e[]数组里,数组的键值为事件的名字,键值的数值为一个Clist对象,Clist是Yii提供的一个队列容器,Clist的方法add()添加事件的回调handle。

//添加一个全局函数到事件处理
$component->onBeginRequest="logRequest";
//添加一个类静态方法到事件处理
$component->onBeginRequest=array("CLog","logRequest");
//添加一个对象方法到事件处理
$component->onBeginRequest=array($mylog,"logRequest");

唤起事件:

$component->raiseEvent('onBeginRequest', $event);

会自动调用:

logRequest($event), Clog::logRequest($event)和$mylog.logRequest($event)

事件句柄必须按照如下来定义 :

function methodName($event)
{
    // ......
}

$event 参数是 CEvent 或其子类的实例,它至少包含了"是谁挂起了这个事件"的信息。

事件的名字以"on"开头,在__get()和__set()里可以通过这个来区别属性和事件。

组件行为(behavior)

组件的行为是一种不通过继承而扩展组件功能的方法(参见设计模式里的策略模式)。

行为类必须实现 IBehavior 接口,大多数行为可以从 CBehavior 基类扩展而来。

IBehavior接口提供了4个方法。

attach($component)将自身关联到组件,detach($component) 解除$component关联,getEnabled()和setEnabled()设置行为对象的有效性。

行为对象存放在组件的$_m[]数组里,数组键值为行为名字符串,数组值为行为类对象。

组件通过attachBehavior ($name,$behavior)来扩展一个行为:

$component->attachBehavior('render',$htmlRender);

为$component添加了一个名字为render的行为,$htmlRender 需是一个实现 IBehavior 接口的对象,或是一个数组:

array( 'class'=>'path.to.BehaviorClass',
    'property1'=>'value1',
    'property2'=>'value2',
    // ... 
);

会根据数组的class来创建行为对象并设置属性值。

$htmlRender被存储到$_m['render']中。

外部调用一个组件未定义的方法时,魔术方法__call() 会遍历所有行为对象,如果找到同名方法就调用之。

例如 $htmlRender 有个方法 renderFromFile(),则可以直接当做组件的方法来访问:

$component->renderFromFile()

CComponent源码分析

//所有部件的基类
class CComponent
{
    private $_e;
    private $_m;
 
    //获取部件属性、事件和行为的magic method
    public function __get($name)
    {
        $getter='get'.$name;
        //是否存在属性的get方法
        if(method_exists($this,$getter))
            return $this->$getter();
        //以on开头,获取事件处理句柄
        else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
        {
            // 事件名小写
            $name=strtolower($name);
            // 如果_e[$name] 不存在,返回一个空的CList事件句柄队列对象
            if(!isset($this->_e[$name]))
                $this->_e[$name]=new CList;
            // 返回_e[$name]里存放的句柄队列对象
            return $this->_e[$name];
        }
        // _m[$name] 里存放着行为对象则返回
        else if(isset($this->_m[$name]))
            return $this->_m[$name];
        else
            throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
                                 array('{class}'=>get_class($this), '{property}'=>$name)));
    }
 
    /**
     * PHP magic method
     * 设置组件的属性和事件
     */
    public function __set($name,$value)
    {
        $setter='set'.$name;
        //是否存在属性的set方法
        if(method_exists($this,$setter))
            $this->$setter($value);
        //name以on开头,这是事件处理句柄
        else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
        {
            // 事件名小写
            $name=strtolower($name);
            // _e[$name] 不存在则创建一个CList对象
            if(!isset($this->_e[$name]))
                $this->_e[$name]=new CList;
            // 添加事件处理句柄
            $this->_e[$name]->add($value);
        }
        // 属性没有set方法,只有get方法,为只读属性,抛出异常
        else if(method_exists($this,'get'.$name))
            throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.',
                                 array('{class}'=>get_class($this), '{property}'=>$name)));
        else
            throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
                                 array('{class}'=>get_class($this), '{property}'=>$name)));
    }
 
    /**
     * PHP magic method
     * 为isset()函数提供是否存在属性和事件处理句柄的判断
     */
    public function __isset($name)
    {
        $getter='get'.$name;
        if(method_exists($this,$getter))
            return $this->$getter()!==null;
        else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
        {
            $name=strtolower($name);
            return isset($this->_e[$name]) && $this->_e[$name]->getCount();
        }
        else
            return false;
    }
 
    /**
     * PHP magic method
     * 设置属性值为空或删除事件名字对应的处理句柄
     */
    public function __unset($name)
    {
        $setter='set'.$name;
        if(method_exists($this,$setter))
            $this->$setter(null);
        else if(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
            unset($this->_e[strtolower($name)]);
        else if(method_exists($this,'get'.$name))
            throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.',
                                 array('{class}'=>get_class($this), '{property}'=>$name)));
    }
 
    /**
     * PHP magic method
     * CComponent未定义的类方法,寻找行为类里的同名方法,实现行为方法的调用
     */
    public function __call($name,$parameters)
    {
        // 行为类存放的$_m数组不空
        if($this->_m!==null)
        {
            // 循环取出$_m数组里存放的行为类
            foreach($this->_m as $object)
            {
                // 行为类对象有效,并且方法存在,调用之
                if($object->enabled && method_exists($object,$name))
                    return call_user_func_array(array($object,$name),$parameters);
            }
        }
        throw new CException(Yii::t('yii','{class} does not have a method named "{name}".',
                             array('{class}'=>get_class($this), '{name}'=>$name)));
    }
 
    /**
     * 根据行为名返回行为类对象
     */
    public function asa($behavior)
    {
        return isset($this->_m[$behavior]) ? $this->_m[$behavior] : null;
    }
 
    /**
     * Attaches a list of behaviors to the component.
     * Each behavior is indexed by its name and should be an instance of
     * {@link IBehavior}, a string specifying the behavior class, or an
     * array of the following structure:
     * <pre>
     * array(
     *     'class'=>'path.to.BehaviorClass',
     *     'property1'=>'value1',
     *     'property2'=>'value2',
     * )
     * </pre>
     * @param array list of behaviors to be attached to the component
     * @since 1.0.2
     */
    public function attachBehaviors($behaviors)
    {
        // $behaviors为数组 $name=>$behavior
        foreach($behaviors as $name=>$behavior)
            $this->attachBehavior($name,$behavior);
    }
 
    /**
     * 添加一个行为到组件
     */
    public function attachBehavior($name,$behavior)
    {
        /* $behavior不是IBehavior接口的实例,则为
         * array(
         *     'class'=>'path.to.BehaviorClass',
         *     'property1'=>'value1',
         *     'property2'=>'value2',
         * )
         * 传递给Yii::createComponent创建行为了并初始化对象属性
         */
        if(!($behavior instanceof IBehavior))
            $behavior=Yii::createComponent($behavior);
        $behavior->setEnabled(true);
        $behavior->attach($this);
        return $this->_m[$name]=$behavior;
    }
 
    /**
     * Raises an event.
     * This method represents the happening of an event. It invokes
     * all attached handlers for the event.
     * @param string the event name
     * @param CEvent the event parameter
     * @throws CException if the event is undefined or an event handler is invalid.
     */
    public function raiseEvent($name,$event)
    {
        $name=strtolower($name);
        // _e[$name] 事件处理句柄队列存在
        if(isset($this->_e[$name]))
        {
            // 循环取出事件处理句柄
            foreach($this->_e[$name] as $handler)
            {
                // 事件处理句柄为全局函数
                if(is_string($handler))
                    call_user_func($handler,$event);
                else if(is_callable($handler,true))
                {
                    // an array: 0 - object, 1 - method name
                    list($object,$method)=$handler;
                    if(is_string($object)) // 静态类方法
                        call_user_func($handler,$event);
                    else if(method_exists($object,$method))
                        $object->$method($event);
                    else
                        throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".',
                                             array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>$handler[1])));
                }
                else
                    throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".',
                                         array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>gettype($handler))));
                // $event 的handled 设置为true后停止队列里剩余句柄的调用
                if(($event instanceof CEvent) && $event->handled)
                    return;
            }
        }
        else if(YII_DEBUG && !$this->hasEvent($name))
            throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.',
                                 array('{class}'=>get_class($this), '{event}'=>$name)));
    }
}

三. 类自动加载与应用组件

Yii应用的入口脚本引用出了Yii类,Yii类的定义:

class Yii extends YiiBase
{
}

由yiic创建的应用里Yii类只是YiiBase类的"马甲",我们也可以根据需求定制自己的Yii类。

Yii(即YiiBase)是一个"helper class",为整个应用提供静态和全局访问入口。

Yii类的几个静态成员:

  • $_aliases : 存放系统的别名对应的真实路径
  • $_imports :
  • $_classes :
  • $_includePaths php include paths
  • $_app : CWebApplication对象,通过 Yii::app() 访问到
  • $_logger : 系统日志对象

$_app 对象由 Yii::createWebApplication() 方法创建。

类自动加载

Yii基于php5的autoload机制来提供类的自动加载功能,自动加载器为YiiBase类的静态方法autoload()。

当程序中用new创建对象或访问到类的静态成员,php将类名传递给类加载器,由类加载器完成类文件的include。

autoload机制实现了类的"按需导入",就是系统访问到类时才include类的文件。

YiiBase类的静态成员$_coreClasses 里预先存放了Yii自身的核心类名于对应的类文件路径。其他的Yii应用中用到的类可以用Yii::import() 导入,Yii::import()将单类的与对应类文件存放于$_classes中,以*通配符表示的路径加入到php include_path中。被Yii::import()导入的类文件或目录都记入$_imports中,避免多次导入。

/* Yii::import()
 * $alias: 要导入的类名或路径
 * $forceInclude false:只导入不include类文件,true则导入并include类文件
 */
public static function import($alias, $forceInclude=false)
{
    // 先判断$alias是否存在于YiiBase::$_imports[] 中,已存在的直接return, 避免重复import。
    if (isset(self::$_imports[$alias])) // previously imported
        return self::$_imports[$alias];
    // $alias类已定义,记入$_imports[],直接返回
    if (class_exists($alias, false) || interface_exists($alias, false))
        return self::$_imports[$alias] = $alias;
    // 已定义于$_coreClasses[]的类,或名字中不含.的类,记入$_imports[],直接返回
    if (isset(self::$_coreClasses[$alias]) || ($pos = strrpos($alias, '.')) === false) // a simple class name
    {
        self::$_imports[$alias] = $alias;
        if ($forceInclude)
        {
            if (isset(self::$_coreClasses[$alias])) // a core class
                require(YII_PATH . self::$_coreClasses[$alias]);
            else
                require($alias . '.php');
        }
        return $alias;
    }
    // 产生一个变量 $className,为$alias最后一个.后面的部分
    // 这样的:'x.y.ClassNamer'
    // $className不等于 '*', 并且ClassNamer类已定义的,      ClassNamer' 记入 $_imports[],直接返回
    if (($className = (string) substr($alias, $pos + 1)) !== '*' && (class_exists($className, false) || interface_exists($className, false)))
        return self::$_imports[$alias] = $className;
    // $alias里含有别名,并转换真实路径成功
    if (($path = self::getPathOfAlias($alias)) !== false)
    {
        // 不是以*结尾的路径(单类)
        if ($className !== '*')
        {
            self::$_imports[$alias] = $className;
            if ($forceInclude)
                require($path . '.php');
            else
            // 类名与真实路径记入$_classes数组
                self::$_classes[$className] = $path . '.php';
            return $className;
        }
        // $alias是'system.web.*'这样的已*结尾的路径,将路径加到include_path中
        else // a directory
        {
            if (self::$_includePaths === null)
            {
                self::$_includePaths = array_unique(explode(PATH_SEPARATOR, get_include_path()));
                if (($pos = array_search('.', self::$_includePaths, true)) !== false)
                    unset(self::$_includePaths[$pos]);
            }
            array_unshift(self::$_includePaths, $path);
            set_include_path('.' . PATH_SEPARATOR . implode(PATH_SEPARATOR, self::$_includePaths));
            return self::$_imports[$alias] = $path;
        }
    }
    else
        throw new CException(Yii::t('yii', 'Alias "{alias}" is invalid. Make sure it points to an existing directory or file.',
                             array('{alias}' => $alias)));
}

然后看看 YiiBase::autoload() 函数的处理:

public static function autoload($className)
{
    // $_coreClasses中配置好的类直接引入
    if(isset(self::$_coreClasses[$className]))
        include(YII_PATH.self::$_coreClasses[$className]);
    // $_classes 中登记的单类直接引入
    else if(isset(self::$_classes[$className]))
        include(self::$_classes[$className]);
    else
    {
        // 其他的认为文件路径以记入 include_path 里,以$className.'.php'直接引入
        include($className.'.php');
        return class_exists($className,false) || interface_exists($className,false);
    }
    return true;
}

系统配置文件里的 import 项里的类或路径在脚本启动中会被自动导入。用户应用里个别类需要引入的类可以在类定义前加入 Yii::import() 语句。

应用组件管理

前面提到Yii的CComponent类提供了组件的属性、事件、行为的访问接口,而CComponent的子类CModule更提供了应用组件(application components)的管理。

应用组件必须是IApplicationComponen接口的实例,需要实现接口的init()和getIsInitialized()方法。init()会在应用组件初始化参数后被自动调用。

Yii自身的功能模块都是通过应用组件的方式来提供的,比如常见的 Yii::app()->user, Yii::app()->request 等。用户也可以定义应用组件。

作为 Yii::app() 对象(CWebApplication)的父类,CModule提供了完整的组件生命周期管理,包括组件的创建、初始化、对象存储等。

每个应用组件用一个字符串名字来标识,通过CModule类的__get() 方法来访问。

CModule类的$_components[] 成员存放应用组件的对象实例($name => $object),$_componentConfig[] 里存放应用组件的类名和初始化参数。

使用应用组件的时候,先在$_componentConfig里设置好组件的类名和初始化参数,在第一次访问组件的时候,CModule会自动创建应用组件对象实例并初始化给定的参数,然后会调用应用组件的init()方法。

Yii::app()对象的类CWebApplication及其父类CApplication预先配置了系统自身用到的应用组件:urlManager, request, session, assetManager, user, themeManager, authManager, clientScript, coreMessages, db, messages, errorHandler, securityManager, statePersister。

我们可以再系统配置文件的components项目里修改系统应用组件的参数或配置新的应用组件。

CModule并不负责应用组件实例的创建,而是由Yii::createComponent() 静态方法来完成的。

createComponent()的参数$config 可以是类名的字符串或是存储了类名和初始化参数的数组。

应用组件的配置

应用组件的配置存储在系统$config变量中(config/main.php里)的components项里:

// application components
'components'=>array(
    'log'=>array(
        'class'=>'CLogRouter',
        'routes'=>array(
            array(
                'class'=>'CFileLogRoute',
                'levels'=>'error, warning',
            ),
        ),
    ),
    'user'=>array(
        // enable cookie-based authentication
        'allowAutoLogin'=>true,
    ),
),

$config里的components项在CApplication的构造函数里被处理:

$this->configure($config);

configure()函数的处理很简单:

public function configure($config)
{
    if(is_array($config))
    {
        foreach($config as $key=>$value)
            $this->$key=$value;
    }
}

$config里的每一项被当做属性传给$_app对象的setXXX()属性设置方法,其中'components'项在CWebApplication的CModule的父类setComponents()处理。

setComponents() 将'components'项里的类名及初始化参数存放到 $_componentConfig[]里:

public function setComponents($components)
{
    // $config 里的'components'每一项
    foreach($components as $id=>$component)
    {
        if($component instanceof IApplicationComponent)
            $this->setComponent($id,$component);
        // $_componentConfig里已经存在配置,合并$component
        else if(isset($this->_componentConfig[$id]))
            $this->_componentConfig[$id]=CMap::mergeArray($this->_componentConfig[$id],$component);
        // 在$_componentConfig里新建项目
        else
            $this->_componentConfig[$id]=$component;
    }
}

应用组件的访问

CModule类重载了CComponent的__get()方法,优先访问应用组件对象。

public function __get($name)
{
    if($this->hasComponent($name))
        return $this->getComponent($name);
    else
        return parent::__get($name);
}

hasComponent() 判断$_components[]中是否已存在组件实例,或$_componentConfig[]中存在组件配置信息。

public function hasComponent($id)
{
    return isset($this->_components[$id]) || isset($this->_componentConfig[$id]);
}

getComponent() 判断组件实例已经存在于$_components[]中,则直接返回对象。 否则根据$_componentConfig[]里的组件配置数据调用 Yii::createComponent() 来创建组件,并将对象存入$_components[]中然后返回。

public function getComponent($id,$createIfNull=true)
{
    if(isset($this->_components[$id]))
        return $this->_components[$id];
    else if(isset($this->_componentConfig[$id]) && $createIfNull)
    {
        $config=$this->_componentConfig[$id];
        unset($this->_componentConfig[$id]);
        if(!isset($config['enabled']) || $config['enabled'])
        {
            Yii::trace("Loading \"$id\" application component",'system.web.CModule');
            unset($config['enabled']);
            $component=Yii::createComponent($config);
            $component->init();
            return $this->_components[$id]=$component;
        }
    }
}

应用组件的创建

Yii::createComponent() 来完成应用组件的创建

public static function createComponent($config)
{
    if (is_string($config))
    {
        $type = $config;
        $config = array();
    }
    else if (isset($config['class']))
    {
        $type = $config['class'];
        unset($config['class']);
    }
    else
        throw new CException(Yii::t('yii', 'Object configuration must be an array containing a "class" element.'));
 
    if (!class_exists($type, false))
        $type = Yii::import($type, true);
 
    if (($n = func_num_args()) > 1)
    {
        $args = func_get_args();
        if ($n === 2)
            $object = new $type($args[1]);
        else if ($n === 3)
            $object = new $type($args[1], $args[2]);
        else if ($n === 4)
            $object = new $type($args[1], $args[2], $args[3]);
        else
        {
            unset($args[0]);
            $class = new ReflectionClass($type);
            // Note: ReflectionClass::newInstanceArgs() is available for PHP 5.1.3+
            // $object=$class->newInstanceArgs($args);
            $object = call_user_func_array(array($class, 'newInstance'), $args);
        }
    }
    else
        $object=new $type;
 
    foreach ($config as $key => $value)
        $object->$key = $value;
 
    return $object;
}

四. 应用程序分析

Yii应用的入口脚本最后一句启动了WebApplication

Yii::createWebApplication($config)->run();

CApplication:

public function run()
{
    $this->onBeginRequest(new CEvent($this));
    $this->processRequest();
    $this->onEndRequest(new CEvent($this));
}

processRequest()开始处理请求,由CWebApplication实现:

public function processRequest()
{
    if(is_array($this->catchAllRequest) && isset($this->catchAllRequest[0]))
    {
        $route=$this->catchAllRequest[0];
        foreach(array_splice($this->catchAllRequest,1) as $name=>$value)
            $_GET[$name]=$value;
    }
    else
        $route=$this->getUrlManager()->parseUrl($this->getRequest());
    $this->runController($route);
}

urlManager应用组件的parseUrl() 创建了$route (形式为controllerID/actionID的字符串),runController()创建Controller对象开始处理http请求。

$route 的值可能存在以下几种情况:

  • 为空: 用 defaultController 值代替;
  • "moduleID/controllerID/actionID" : module下的
  • "controllerID/actionID" : 最常见的形式
  • "folder1/folder2/controllerID/actionID" : 多级目录下的控制器

runController()

runController首先调用createController()创建控制器对象

public function createController($route, $owner=null)
{
    // $owner为空则设置为$this,即 $_app对象
    if ($owner === null)
        $owner = $this;
    // $route为空设置为defaultController,在$config里配置
    if (($route = trim($route, '/')) === '')
        $route = $owner->defaultController;
    $caseSensitive = $this->getUrlManager()->caseSensitive;
 
    $route.='/';
    // 逐一取出 $route 按 ‘/’分割后的第一段进行处理
    while (($pos = strpos($route, '/')) !== false)
    {
        // $id 里存放的是 $route 第一个 ‘/’前的部分
        $id = substr($route, 0, $pos);
        if (!preg_match('/^\w+$/', $id))
            return null;
        if (!$caseSensitive)
            $id = strtolower($id);
        // $route 存放’/’后面部分
        $route = (string) substr($route, $pos + 1);
        if (!isset($basePath)) // 完整$route的第一段
        {
            // 如果$id在controllerMap[]里做了映射
            // 直接根据$id创建controller对象
            if (isset($owner->controllerMap[$id]))
            {
                return array(
                    Yii::createComponent($owner->controllerMap[$id], $id, $owner === $this ? null : $owner),
                    $this->parseActionParams($route),
                );
            }
 
            // $id 是系统已定义的 module,根据$id取得module对象作为$owner参数来createController
            if (($module = $owner->getModule($id)) !== null)
                return $this->createController($route, $module);
            // 控制器所在的目录
            $basePath = $owner->getControllerPath();
            $controllerID = '';
        }
        else
            $controllerID.='/';
        $className = ucfirst($id) . 'Controller';
        $classFile = $basePath . DIRECTORY_SEPARATOR . $className . '.php';
        // 控制器类文件存在,则require并创建控制器对象&返回
        if (is_file($classFile))
        {
            if (!class_exists($className, false))
                require($classFile);
            if (class_exists($className, false) && is_subclass_of($className, 'CController'))
            {
                $id[0] = strtolower($id[0]);
                return array(
                    new $className($controllerID . $id, $owner === $this ? null : $owner),
                    $this->parseActionParams($route),
                );
            }
            return null;
        }
        // 未找到控制器类文件,可能是多级目录,继续往子目录搜索
        $controllerID.=$id;
        $basePath.=DIRECTORY_SEPARATOR . $id;
    }
}

createController() 返回一个创建好的控制器对象和actionID, runController()调用控制器的init()方法和run($actionID)来运行控制器:

public function runController($route)
{
    if (($ca = $this->createController($route)) !== null)
    {
        list($controller, $actionID) = $ca;
        $oldController = $this->_controller;
        $this->_controller = $controller;
        $controller->init();
        $controller->run($actionID);
        $this->_controller = $oldController;
    }
    else
        throw new CHttpException(404, Yii::t('yii', 'Unable to resolve the request "{route}".', array('{route}' => $route === '' ? $this->defaultController : $route)));
}

$controller->init()里没有动作, run():

public function run($actionID)
{
    if (($action = $this->createAction($actionID)) !== null)
    {
        if (($parent = $this->getModule()) === null)
            $parent = Yii::app();
        if ($parent->beforeControllerAction($this, $action))
        {
            $this->runActionWithFilters($action, $this->filters());
            $parent->afterControllerAction($this, $action);
        }
    }
    else
        $this->missingAction($actionID);
}

runAction()

$controller->run($actionID)里首先创建了Action对象:

public function createAction($actionID)
{
    // 为空设置为defaultAction
    if ($actionID === '')
        $actionID = $this->defaultAction;
    // 控制器里存在 'action'.$actionID 的方法,创建CInlineAction对象
    if (method_exists($this, 'action' . $actionID) && strcasecmp($actionID, 's')) // we have actions method
        return new CInlineAction($this, $actionID);
    // 否则根据actions映射来创建Action对象
    else
        return $this->createActionFromMap($this->actions(), $actionID, $actionID);
}

这里可以看到控制器并不是直接调用了action方法,而是需要一个Action对象来运行控制器动作,这样就统一了控制器方法和actions映射的action对象对action的处理,即两种形式的action处理都统一为IAction接口的run()调用。

IAction接口要求实现run(),getId(),getController () 三个方法,Yii提供的CAction类要求构造函数提供Controller和Id并实现了getId()和getController ()的处理,Action类从CAction继承即可。

CInlineAction在web/action下,run()是很简单的处理过程,调用了Controller的action方法:

class CInlineAction extends CAction
{
 
    public function run()
    {
        $method = 'action' . $this->getId();
        $this->getController()->$method();
    }
 
}

回到 $controller->run($actionID)

public function run($actionID)
{
    if (($action = $this->createAction($actionID)) !== null)
    {
        if (($parent = $this->getModule()) === null)
            $parent = Yii::app();
        if ($parent->beforeControllerAction($this, $action))
        {
            $this->runActionWithFilters($action, $this->filters());
            $parent->afterControllerAction($this, $action);
        }
    }
    else
        $this->missingAction($actionID);
}

Yii::app()->beforeControllerAction() 实际是固定返回true的,所以action对象实际是通过控制器的runActionWithFilters()被run的

public function runActionWithFilters($action, $filters)
{
    // 控制器里没有设置过滤器
    if (empty($filters))
        $this->runAction($action);
    else
    {
        // 创建过滤器链对象并运行
        $priorAction = $this->_action;
        $this->_action = $action;
        CFilterChain::create($this, $action, $filters)->run();
        $this->_action = $priorAction;
    }
}

没有过滤器,runAction()就是最终要调用前面创建的action对象的run()方法:

public function runAction($action)
{
    $priorAction = $this->_action;
    $this->_action = $action;
    if ($this->beforeAction($action))
    {
        $action->run();
        $this->afterAction($action);
    }
    $this->_action = $priorAction;
}

每个filter都要实现IFilter接口,filter实现的preFilter()方法在$action->run()之前调用,如果判断action可以执行则返回true,否则返回false

if ($filter1->preFilter())
    if ($filter2->preFilter())
        if ($filtern->preFilter())
            $action->run()
$filtern->postFilter()
$filter2->postFilter()
$filter1->postFilter()

render view

在action里最常见的操作就是render view文件: renderPartial()和render()。render()在处理view文件后会把结果放入layout文件内。

public function renderPartial($view, $data=null, $return=false, $processOutput=false)
{
    if (($viewFile = $this->getViewFile($view)) !== false)
    {
        $output = $this->renderFile($viewFile, $data, true);
        if ($processOutput)
            $output = $this->processOutput($output);
        if ($return)
            return $output;
        else
            echo $output;
    }
    else
        throw new CException(Yii::t('yii', '{controller} cannot find the requested view "{view}".',
                             array('{controller}' => get_class($this), '{view}' => $view)));
}

getViewFile($view)获得$view的完整路径: $view 以 '/' 开头的,以系统views目录作为起始目录+$view+.php $view含有别名的,查找别名的真实路径 其他的以modele view目录作为起始目录+$view+.php

如果没有在$config里配置第三方的renderer,renderFile() 里实际是调用了yii自身提供的renderInternal()来render view文件:

public function renderFile($viewFile, $data=null, $return=false)
{
    $widgetCount = count($this->_widgetStack);
    // 如果配置了其他的ViewRenderer
    if (($renderer = Yii::app()->getViewRenderer()) !== null)
        $content = $renderer->renderFile($this, $viewFile, $data, $return);
    else
    // yii 自身的render
        $content=$this->renderInternal($viewFile, $data, $return);
    if (count($this->_widgetStack) === $widgetCount)
        return $content;
    else
    {
        $widget = end($this->_widgetStack);
        throw new CException(Yii::t('yii', '{controller} contains improperly nested widget tags in its view "{view}". A {widget} widget does not have an                 endWidget() call.',
                array('{controller}' => get_class($this), '{view}' => $viewFile, '{widget}' => get_class($widget))));
    }
}

Yii的renderer用的是php本身作为模板系统:

public function renderInternal($_viewFile_, $_data_=null, $_return_=false)
{
    // extract函数将$_data_从数组中将变量导入到当前的符号表
    if (is_array($_data_))
        extract($_data_, EXTR_PREFIX_SAME, 'data');
    else
        $data=$_data_;
    if ($_return_)
    {
        ob_start();
        ob_implicit_flush(false);
        require($_viewFile_);
        return ob_get_clean();
    }
    else
        require($_viewFile_);
}

render()的实际上是先renderPartial view文件,然后renderFile layoutfile,并将view文件的结果做为$content变量传入。

public function render($view, $data=null, $return=false)
{
    $output = $this->renderPartial($view, $data, true);
    if (($layoutFile = $this->getLayoutFile($this->layout)) !== false)
        $output = $this->renderFile($layoutFile, array('content' => $output), true);
 
    $output = $this->processOutput($output);
 
    if ($return)
        return $output;
    else
        echo $output;
}

processOutput将render的结果再做处理,比如在head加上css或js脚本等。

public function processOutput($output)
{
    Yii::app()->getClientScript()->render($output);
 
    // if using page caching, we should delay dynamic output replacement
    if ($this->_dynamicOutput !== null && $this->isCachingStackEmpty())
        $output = $this->processDynamicOutput($output);
 
    if ($this->_pageStates === null)
        $this->_pageStates = $this->loadPageStates();
    if (!empty($this->_pageStates))
        $this->savePageStates($this->_pageStates, $output);
 
    return $output;
}

转载自

develop/yiiframework-research.txt · 最后更改: 2010/11/08 03:36 由 lostsnow
 
Recent changes RSS feed Donate Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki