• 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的实时消息推送一文中的示例。

  • 相关阅读:
    javascript执行上下文
    javascript深浅拷贝
    javascript模块化
    javascript类型转换
    闭包
    通过插槽分发内容
    组件上使用v-model
    Vue $emit $event 传值(子to父)
    Vue Prop属性(父to子)
    Vue组件全局/局部注册
  • 原文地址:https://www.cnblogs.com/sunscheung/p/4864144.html
Copyright © 2020-2023  润新知