zoukankan      html  css  js  c++  java
  • Yii源码阅读笔记

    2014-11-13 四

    By youngsterxyf

    概述

    Yii框架将各种功能封装成组件,使用时按需配置加载,从而提高应用的性能。内置的组件又分为核心组件与非核心组件,核心组件是任何Web应用和Console应用都需要的。 此外,应用开发者还可以按照一定规则封装配置使用自己的功能组件。Yii会把应用需要的组件都加载到应用容器Yii::app()中,使得组件的使用方式一致方便。

    基于Yii框架开发应用需要理解如何配置组件、如何开发自己的组件,对应着需要理解Yii是如何注册加载组件的。

    分析

    Yii源码阅读笔记 - 请求处理基本流程一文可知,Yii加载组件的入口为抽象类CApplication构造方法中的以下两行代码:

    $this->registerCoreComponents();
    $this->configure($config);
    

    registerCoreComponents方法定义于类CWebApplication中,用于加载Web应用的核心组件,组件列表如下:

    array(
        // 核心组件
        'coreMessages'=>array(
            'class'=>'CPhpMessageSource',
            'language'=>'en_us',
            'basePath'=>YII_PATH.DIRECTORY_SEPARATOR.'messages',
        ),
        'db'=>array(
            'class'=>'CDbConnection',
        ),
        'messages'=>array(
            'class'=>'CPhpMessageSource',
        ),
        'errorHandler'=>array(
            'class'=>'CErrorHandler',
        ),
        'securityManager'=>array(
            'class'=>'CSecurityManager',
        ),
        'statePersister'=>array(
            'class'=>'CStatePersister',
        ),
        'urlManager'=>array(
            'class'=>'CUrlManager',
        ),
        'request'=>array(
            'class'=>'CHttpRequest',
        ),
        'format'=>array(
            'class'=>'CFormatter',
        ),
    
        // 以下是Web应用额外需要的核心组件
        'session'=>array(
            'class'=>'CHttpSession',
        ),
        'assetManager'=>array(
            'class'=>'CAssetManager',
        ),
        'user'=>array(
            'class'=>'CWebUser',
        ),
        'themeManager'=>array(
            'class'=>'CThemeManager',
        ),
        'authManager'=>array(
            'class'=>'CPhpAuthManager',
        ),
        'clientScript'=>array(
            'class'=>'CClientScript',
        ),
        'widgetFactory'=>array(
            'class'=>'CWidgetFactory',
        ),
    )
    

    注册加载组件都是直接调用方法setComponents,间接调用方法setComponent来完成的。


    configure方法定义于类CModule中,是用于加载所有配置信息的,实现如下:

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

    Yii源码阅读笔记 - 请求处理基本流程一文可知,配置信息的加载是基于类CComponent中的魔术方法__set来完成的,该方法实现如下:

    public function __set($name,$value)
    {
        // PHP的类名、函数名、方法名都是不区分大小写的!
        $setter='set'.$name;
        if(method_exists($this,$setter))
            return $this->$setter($value);
        elseif(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;
            return $this->_e[$name]->add($value);
        }
        elseif(is_array($this->_m))
        {
            foreach($this->_m as $object)
            {
                if($object->getEnabled() && (property_exists($object,$name) || $object->canSetProperty($name)))
                    return $object->$name=$value;
            }
        }
        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)));
    }
    

    而类CModule中又定义了方法setComponents,所以对于key为components的配置项,也是调用方法setComponents,间接调用方法setComponent来完成的。

    方法setComponent实现如下:

    /**
     * Puts a component under the management of the module.
     * The component will be initialized by calling its {@link CApplicationComponent::init() init()}
     * method if it has not done so.
     * @param string $id component ID
     * @param array|IApplicationComponent $component application component
     * (either configuration array or instance). If this parameter is null,
     * component will be unloaded from the module.
     * @param boolean $merge whether to merge the new component configuration
     * with the existing one. Defaults to true, meaning the previously registered
     * component configuration with the same ID will be merged with the new configuration.
     * If set to false, the existing configuration will be replaced completely.
     * This parameter is available since 1.1.13.
     */
    public function setComponent($id,$component,$merge=true)
    {
        if($component===null)
        {
            unset($this->_components[$id]);
            return;
        }
        elseif($component instanceof IApplicationComponent)
        {
            $this->_components[$id]=$component;
    
            if(!$component->getIsInitialized())
                $component->init();
    
            return;
        }
        elseif(isset($this->_components[$id]))
        {
            if(isset($component['class']) && get_class($this->_components[$id])!==$component['class'])
            {
                unset($this->_components[$id]);
                $this->_componentConfig[$id]=$component; //we should ignore merge here
                return;
            }
    
            foreach($component as $key=>$value)
            {
                if($key!=='class')
                    $this->_components[$id]->$key=$value;
            }
        }
        // 以configure方法为入口的组件注册可能走的分支
        elseif(isset($this->_componentConfig[$id]['class'],$component['class'])
            && $this->_componentConfig[$id]['class']!==$component['class'])
        {
            $this->_componentConfig[$id]=$component; //we should ignore merge here
            return;
        }
    
        // 以configure方法为入口的组件注册可能走的分支
        if(isset($this->_componentConfig[$id]) && $merge)
            // 对组件的信息进行合并,即意味着如果是对核心组件做额外配置,可以不用指定class等信息。
            $this->_componentConfig[$id]=CMap::mergeArray($this->_componentConfig[$id],$component);
        else
            // 核心组件注册全走这个分支
            // 非核心组件、自定义组件注册走这个分支
            $this->_componentConfig[$id]=$component;
    }
    

    对于以registerCoreComponents方法、configure方法为入口的组件注册,调用setComponent方法时的参数$component是一个数组。

    注册核心组件前,应用对象的属性_component_componentConfig都为空,所以核心组件注册最终走的都是最后一个else分支

    由于可以配置与核心组件相同ID的组件,比如db,那么注册配置的组件(以configure方法为入口)走的是最后一个elseif分支或者最后一个if分支

    可以看到以这两个方法为入口的组件注册都没有对组件进行初始化。那么什么时候初始化组件的呢?只能是调用组件的时候了。


    组件是通过应用对象容器来调用的。以db组件为例,调用方式为:Yii::app()->db,但实际是基于魔术方法__get来完成的,该魔术方法定义于类CModule中,实现如下:

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

    先尝试查找对应$name的组件。从这里可以看出Web应用容器中除了存组件,还可以存其他信息,如所有的配置信息。

    方法hasComponent实现如下:

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

    之所以会先查看属性_components,是因为_components中保存的组件是已经加载好的,而_componentConfig保存的是所有注册的组件,但未初始化。即_components中的组件是_componentConfig中组件的子集,检测起来会更快?我的理解是这样的。

    方法getComponent实现如下:

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

    先查看属性_components中是否已保存初始化好的对应组件,是,则直接取出来返回,这样重复调用相同组件只会初始化一次;否,则对该组件进行初始化。

    组件初始化分为两个步骤:

    1. Yii根据组件的配置信息实例化一个组件对象,即$component=Yii::createComponent($config)
    2. 组件对象调用自己的方法init完成一些初始化操作,即$component->init()

    初始化结束后,将组件对象存入属性_components中。


    静态方法createComponent定义于类YiiBase中,实现如下:

    /**
     * Creates an object and initializes it based on the given configuration.
     *
     * The specified configuration can be either a string or an array.
     * If the former, the string is treated as the object type which can
     * be either the class name or {@link YiiBase::getPathOfAlias class path alias}.
     * If the latter, the 'class' element is treated as the object type,
     * and the rest of the name-value pairs in the array are used to initialize
     * the corresponding object properties.
     *
     * Any additional parameters passed to this method will be
     * passed to the constructor of the object being created.
     *
     * @param mixed $config the configuration. It can be either a string or an array.
     * @return mixed the created object
     * @throws CException if the configuration does not have a 'class' element.
     */
    public static function createComponent($config)
    {
        // 如果传入的组件配置信息是字符串类型,则认为是对象类型
        if(is_string($config))
        {
            $type=$config;
            $config=array();
        }
        // 如果是数组,则必须指定组件所对应的class
        elseif(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);
    
        // 如果除了$config,还传递了其他参数,则根据额外的参数来实例化。对于组件初始化来说,不会走这个分支
        if(($n=func_num_args())>1)
        {
            $args=func_get_args();
            if($n===2)
                $object=new $type($args[1]);
            elseif($n===3)
                $object=new $type($args[1],$args[2]);
            elseif($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;
    
        // $config中除了class外的其他字段都作为组件对象的属性进行赋值
        foreach($config as $key=>$value)
            $object->$key=$value;
    
        return $object;
    }
    

    从上述代码可以看出,在配置组件时,如果是配置核心组件,可以不提供class字段,否则一定要提供。除了class字段,还可以为组件对象的属性赋值。按照PHP中对一个对象的属性进行赋值的规则:

    1. 如果该对象有public的该属性,则直接赋值
    2. 否则看该对象所在继承树上是否有定义魔术方法__set,如果有则调用__set来处理赋值过程
    3. 如果连__set也没有,则为该对象生成一个public的属性,然后赋值给它

    可以将自定义组件类需要初始化赋值的属性:

    1. 定义为public访问控制
    2. 如果非public,则应该魔术方法__set
    3. 也可以不定义该属性(我觉得还是定义一下比较好,否则不好理解)

    在静态方法createComponent返回组件对象后,接着调用组件对象自身的init方法来完成一些初始化工具。这也就意味着自定义组件需要有init方法。

    从核心组件的定义可以看到,组件应该继承自抽象类CApplicationComponent(见文件yii/framework/base/CApplicationComponent.php)。该类定义了方法init和getIsInitialized。 自定义组件继承自CApplicationComponent,若没有额外的初始化操作,也可以不再定义自己的init方法。如果定义自己的init方法,最好也间接调用一下父类的init方法(parent::init()), 从而避免一些可能潜在的兼容问题。

    关于自定义组件的更多具体细节,可以参考基于socket.io的实时消息推送一文中的示例。

  • 相关阅读:
    HAProxy、Keepalived 在 Ocatvia 的应用实现与分析
    Octavia 的 HTTPS 与自建、签发 CA 证书
    Octavia 创建 loadbalancer 的实现与分析
    OpenStack Rally 质量评估与自动化测试利器
    自建 CA 中心并签发 CA 证书
    Failed building wheel for netifaces
    通过 vSphere WS API 获取 vCenter Datastore Provisioned Space 置备空间
    OpenStack Placement Project
    我们建了一个 Golang 硬核技术交流群(内含视频福利)
    没有图形界面的软件有什么用?
  • 原文地址:https://www.cnblogs.com/sunscheung/p/4864144.html
Copyright © 2011-2022 走看看