作者:wdy 基于yii1.0.8
网站的唯一入口程序 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类型。
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')
回到前面的程序入口的 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,指向对象自身提供的两个方法。
$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())创建。
继续, $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() 等方法里进行处理。
其中 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))); }
$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[]中。
这个很简单
public function setParams($value) { $params=$this->getParams(); foreach($value as $k=>$v) $params->add($k,$v); }
configure 完毕!
$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]中。
$this->init();
函数内:$this->getRequest(); 创建了Reques 组件并初始化。
public function run() { $this->onBeginRequest(new CEvent($this)); $this->processRequest(); $this->onEndRequest(new CEvent($this)); }
Yii是基于组件(component-based)的web框架,CComponent类是所有组件的基类。
CComponent类为子类提供了基于属性(property)、事件(event)、行为(behavior)编程接口。
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; }
组件的属性值是大小写不敏感的(类的成员时大小写敏感的)
组件事件是一种特殊的属性,它可以将事件处理句柄(可以是函数名、类方法或对象方法)注册(绑定)到一个事件名上,句柄在事件被唤起的时候被自动调用。 组件事件存放在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()里可以通过这个来区别属性和事件。
组件的行为是一种不通过继承而扩展组件功能的方法(参见设计模式里的策略模式)。
行为类必须实现 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()
//所有部件的基类 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类的几个静态成员:
$_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();
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 的值可能存在以下几种情况:
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); }
$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()
在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; }