• Yii2之组件的注册与创建


      今天本来打算研究一下yii2.0AR模型的实现原理,然而,计划赶不上变化,突然就想先研究一下yii2.0的数据库组件创建的过程。通过对yii源码的学习,了解了yii组件注册与创建的过程,并发现原来yii组件注册之后并不是马上就去创建的,而是待到实际需要使用某个组件的时候再去创建对应的组件实例的。本文大概记录一下这个探索的过程。

      要了解yii组件的注册与创建,当然要从yii入口文件index.php说起了,整个文件代码如下:

    <?php
    defined('YII_DEBUG') or define('YII_DEBUG', true);
    defined('YII_ENV') or define('YII_ENV', 'dev');
    
    require(__DIR__ . '/../../vendor/autoload.php');
    require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
    require(__DIR__ . '/../../common/config/bootstrap.php');
    require(__DIR__ . '/../config/bootstrap.php');
    
    $config = yiihelpersArrayHelper::merge(
        require(__DIR__ . '/../../common/config/main.php'),
        require(__DIR__ . '/../../common/config/main-local.php'),
        require(__DIR__ . '/../config/main.php'),
        require(__DIR__ . '/../config/main-local.php')
    );
    
    (new yiiwebApplication($config))->run();

    可以看到入口文件引入了几个配置文件,并将所有配置文件的内容都合并到$config这个配置数组中,然后使用这个配置数组作为参数去创建一个应用实例。若将这个配置数组打印出来,就会看到,“components”下标对应的元素包含了yii组件的参数信息(这里只截图一小部分):

    这些组件的信息是在引入进来的几个配置文件中配置的,Yii组件就是使用这些参数信息进行注册与创建的。

      接下来就进入yiiwebApplication类的实例化过程了,yiiwebApplication类没有构造函数,但是它继承了yiiaseApplication类:

    所以会自动执行yiiaseApplication类的构造函数:

    public function __construct($config = [])
    {
        Yii::$app = $this;
        static::setInstance($this);
    
        $this->state = self::STATE_BEGIN;
    
        $this->preInit($config);
    
        $this->registerErrorHandler($config);
    
        Component::__construct($config);
    }

    这里要顺便说一下预初始化方法preInit(),它的代码如下:

    public function preInit(&$config)
    {
        /* 此处省略对$config数组的预处理操作代码 */
    
        // merge core components with custom components
        foreach ($this->coreComponents() as $id => $component) {
            if (!isset($config['components'][$id])) {
                $config['components'][$id] = $component;
            } elseif (is_array($config['components'][$id]) && !isset($config['components'][$id]['class'])) {
                $config['components'][$id]['class'] = $component['class'];
            }
        }
    }

      这个函数对传递给构造函数的配置数组$config进行了一些预处理操作(这里省略了),最后使用coreComponents()方法返回的数组对$config数组进行了完善,coreComponents()方法是这样的:

    public function coreComponents()
    {
        return [
            'log' => ['class' => 'yiilogDispatcher'],
            'view' => ['class' => 'yiiwebView'],
            'formatter' => ['class' => 'yiii18nFormatter'],
            'i18n' => ['class' => 'yiii18nI18N'],
            'mailer' => ['class' => 'yiiswiftmailerMailer'],
            'urlManager' => ['class' => 'yiiwebUrlManager'],
            'assetManager' => ['class' => 'yiiwebAssetManager'],
            'security' => ['class' => 'yiiaseSecurity'],
        ];
    }

      其实就是一些核心组件的配置,也就是说这些组件是可以不需要我们在配置文件中配置的,yii会自动进行注册。

      好了,回到yiiaseApplication类的构造函数,这个函数最后调用了yiiaseComponent类的构造函数,但yiiaseComponent类是没有构造函数的,不过它继承了yiiaseObject类:

    所以也自动执行了yiiaseObject类的构造函数:

    public function __construct($config = [])
    {
        if (!empty($config)) {
            Yii::configure($this, $config);
        }
        $this->init();
    }

    这里主要是调用了yiiBaseYii类的静态方法configure()

    public static function configure($object, $properties)
    {
        foreach ($properties as $name => $value) {
            $object->$name = $value;
        }
    
        return $object;
    }

    这个方法就是循环入口文件(new yiiwebApplication($config))->run();中的$config数组(这个数组的结构参见本文第一个截图),以数组键名作为对象属性名,对应的键值作为对象属性值进行赋值操作。所以当循环到组件配置参数的时候是这样子的:$object->components = $value$value为所有组件的配置数组),也就是对$objectcomponents属性进行赋值操作,那这个$object是哪个类的对象呢?回想最初调用的源头,其实它就是入口文件中需要进行实例化的yiiwebApplication类的对象啊。然而,这个类和它的祖先类都没有components这个成员变量啊,不急,又要进行一番继承套路了,顺着yiiwebApplication类的继承关系一层一层往上找可以发现yiiwebApplication类最终也继承了yiiaseObject类,yiiaseObject类是支持属性的,所以yiiwebApplication类也支持属性(关于属性,可以参考我的另一篇博文:yii2之属性),当赋值操作找不到components成员变量时会调用setComponents()方法,又去找这个方法的所在,终于在它的祖先类yiidiServiceLocator中找到了setComponents()方法,没错,对应用实例的components属性进行赋值操作其实就是调用这个方法!

      好了,现在就来看看setComponents()这个方法到底干了啥:

    public function setComponents($components)
    {
        foreach ($components as $id => $component) {
            $this->set($id, $component);
        }
    }

    其实很简单,就是循环各个组件的配置数组,调用set()方法,set()方法如下:

    public function set($id, $definition)
    {
        unset($this->_components[$id]);
    
        if ($definition === null) {
            unset($this->_definitions[$id]);
            return;
        }
    
        if (is_object($definition) || is_callable($definition, true)) {
            // an object, a class name, or a PHP callable
            $this->_definitions[$id] = $definition;
        } elseif (is_array($definition)) {
            // a configuration array
            if (isset($definition['class'])) {
                $this->_definitions[$id] = $definition;
            } else {
                throw new InvalidConfigException("The configuration for the "$id" component must contain a "class" element.");
            }
        } else {
            throw new InvalidConfigException("Unexpected configuration type for the "$id" component: " . gettype($definition));
        }
    }

    其实就是把组件配置存入$_definitions这个私有成员变量(即注册),然后呢?然后就没有下文了。。。

      搞了半天,原来yii创建应用实例的时候只是进行组件的注册,并没有实际创建组件,那么组件实例是什么时候进行创建的?在哪里进行创建的呢?别急。从上面推导的这个过程我们知道yiidiServiceLocator类是yiiwebApplication类的祖先类,所以其实yii的应用实例其实就是一个服务定位器,比如我们想访问数据库组件的时候,我们可以这样来访问:Yii::$app->db,这个Yii::$app就是yii应用实例,也就是yiiwebApplication类的实例,但是yiiwebApplication类和它的父类、祖先类都找不到db这个属性啊。哈哈,别忘了,php读取不到类属性的时候会调用魔术方法__get(),所以开始查找yiiwebApplication继承关系最近的祖先类中的__get()方法,最后在yiidiServiceLocator类中找到了,也就是说,Yii::$app->db最终会调用yiidiServiceLocator类中的__get()方法:

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

    __get()方法首先调用has()方法(这个不再贴代码了)判断组件是否已注册,若已注册则调用get()方法:

    public function get($id, $throwException = true)
    {
        if (isset($this->_components[$id])) {
            return $this->_components[$id];
        }
    
        if (isset($this->_definitions[$id])) {
            $definition = $this->_definitions[$id];
            if (is_object($definition) && !$definition instanceof Closure) {
                return $this->_components[$id] = $definition;
            } else {
                return $this->_components[$id] = Yii::createObject($definition);
            }
        } elseif ($throwException) {
            throw new InvalidConfigException("Unknown component ID: $id");
        } else {
            return null;
        }
    }

    其中私有成员变量$_components是存储已经创建的组件实例的,若发现组件已经创建过则直接返回组件示例,否则使用$_definitions中对应组件的注册信息,调用yiiBaseYii::createObject()方法进行组件创建,这个方法最终会调用依赖注入容器yiidiContainerget()方法,接着就是依赖注入创建对象的过程了,关于这个过程已经在我的上一篇博文中讲解过了,可以参考一下:yii2之依赖注入与依赖注入容器

      好了,yii组件注册与创建的整个过程就是这样的。最后总结一下,其实yii创建应用实例的时候只是进行了各个组件的注册,也就是将组件的配置信息存入yiidiServiceLocator类的私有成员变量$_definitions中,并没有进行实际创建,等到程序运行过程中真正需要使用到某个组件的时候才根据该组件在$_definitions中保存的注册信息使用依赖注入容器yiidiContainer进行组件实例的创建,然后把创建的实例存入私有成员变量$_components,这样下次访问相同组件的时候就可以直接返回组件实例,而不再需要执行创建过程了。yii的这个组件注册与创建机制其实是大有裨益的,试想一下,如果在应用实例创建的时候就进行所有组件的创建,将会大大增加应用实例创建的时间,用户每次刷新页面都会进行应用实例的创建的,也就是说用户每刷新一次页面都很慢,这用户体验就很不好了,而且很多情况下有很多组件其实是没有使用到的,但是我们还是花了不少时间去创建这些组件,这是很不明智的,所以yii的做法就是:先把组件参数信息保存起来,需要使用到哪些组件再去创建相应的实例,大大节省了应用创建的时间,同时也节省了内存,这种思路是很值得我们学习的!

  • 相关阅读:
    我终于会手打lct了!
    [模板]Dijkstra-优先队列优化-单源最短路
    99999999海岛帝国后传:算法大会
    正在加载中。。。。。
    【题解】CF1054D Changing Array(异或,贪心)
    【题解】P4550 收集邮票(概率期望,平方期望)
    【题解】CF149D Coloring Brackets(区间 DP,记忆化搜索)
    【笔记】斜率优化 DP
    CSP2021 游记
    【题解】洛谷P1502 窗口的星星
  • 原文地址:https://www.cnblogs.com/wujuntian/p/7745136.html
Copyright © 2020-2023  润新知