❥(^_-) Yii2框架源码解析之容器类Container.php

容器概述

网上总有些人把php框架中的容器说的很高大上。php中的容器其实很简单。

首先,php中的容器是为了解决类和类之间的依赖关系的。举个栗子:

存在三个类:

class Group
{
    public static $a = 0;

    function __construct($a)
    {
        static::$a = $a;
    }
}


class User
{
    public function __construct(Group $group)
    {

    }

}


class UserList
{
    public function __construct(User $user)
    {

    }

    public function getUserList()
    {
        echo "this is the user-list";
    }
}

如果我们要调用UserList类里面的getUserList()方法,通常的做法是:

$group = new Group(1);

$user = new User($group);

$userList = new UserList($user);

$userList->getUserList();

如果我们使用容器的话,需要这样做:

$container = new Container();
$container->set('Group', 'app\service\Group');
$container->set('User', 'app\service\User');
$container->set('UserList', 'app\service\UserList');
$lister = $container->get('UserList');
$lister->getUserList();

我们发现:我们只需要把所有的相关联的类,全部注入到容器中,不需要关心他们之间的依赖关系。(容器帮我们完成了依赖关系的处理)

怎么实现的呢?通过php的反射

php的反射可以获取类的相关信息。那么我们首先通过

$reflection = new \ReflectionClass($className)获取目标类的反射类。再通过
$reflection->getConstructor()获取反射类的构造方法类,再通过
$constructorParameters = $constructor->getParameters()获取构造方法所需参数类。(是一个数组)

再通过参数类的 isDefaultValueAvailable() 方法判断,参数是否有可用默认值。如果没有,则断言它是一个对象,我们就创建它。具体实现看代码:

源码分析

<?php

namespace yii\di;

use ReflectionClass;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;

class Container extends Component
{
 
    private $_singletons = [];
    
    private $_definitions = [];

    private $_params = [];

    private $_reflections = [];
   
    private $_dependencies = [];

    public function get($class, $params = [], $config = [])
    {
        // 这里是获取一个类的实例。
        //$class 第一个参数是类的名称或者别名,当然,你得先使用set()方法将改类注册到容器中,才能使用get方法获取。
        //$params 第二个参数是该类的构造方法所需要的参数,请在数组中按照顺序传入。
        //$config 第三个参数是该类中属性的初始值得设置,是键值对形式的数组。

        // Instance表示对容器中对象的引用
        // 如果获取的当前类属于instance, 唯一标识就变为当前类中的id属性。
        if ($class instanceof Instance) {
            $class = $class->id;
        }

        // singleton属性是一个容器的注册树,如果说当前实例已经存在注册树上,就直接返回。
        if (isset($this->_singletons[$class])) {
            // singleton
            return $this->_singletons[$class];
        } elseif (!isset($this->_definitions[$class])) {
            // 如果不存在定义就创建实例,并返回(创建中会将其注册到全局树上)
            return $this->build($class, $params, $config);
        }

        // 存在$definition的逻辑
        // 这个属性其实就是注册类的时候(调用set()方法),传入的第二个参数。他一共有三种类型。
        // 1.回调函数
        // 2. 数组
        // 3. 字符串。
        $definition = $this->_definitions[$class];

        if (is_callable($definition, true)) {
            // 处理参数
            $params = $this->resolveDependencies($this->mergeParams($class, $params));
            // 回调函数返回值就是当前需要的实例。
            $object = call_user_func($definition, $this, $params, $config);

        } elseif (is_array($definition)) {
            $concrete = $definition['class'];
            unset($definition['class']);
            // 先将参数合并
            $config = array_merge($definition, $config);

            // 将容器中已经注册的类参数和现在传入的参数合并。
            $params = $this->mergeParams($class, $params);
            if ($concrete === $class) {
                // 如果名称一样,就直接创建这个对象。
                $object = $this->build($class, $params, $config);
            } else {
                // 如果不一样,说明是别名,根据正确的类名获取实例。
                $object = $this->get($concrete, $params, $config);
            }
        } elseif (is_object($definition)) {
            // 如果是对象,直接注册到全局实例树。并返回。
            return $this->_singletons[$class] = $definition;
        } else {
            throw new InvalidConfigException('Unexpected object definition type: ' . gettype($definition));
        }

        // 如果是使用setSingleton()方法注册的对象,那么将singletons赋值。
        if (array_key_exists($class, $this->_singletons)) {
            // singleton
            $this->_singletons[$class] = $object;
        }

        return $object;
    }

    public function set($class, $definition = [], array $params = [])
    {
        // 将类注册到容器中。
        // 设置类的定义,类的参数。
        // 注销已经存在类实例。(因为重新注册之后,如果不销毁之前的实例,当类的参数不一样的时候,会导致获取的对象是之前注册的类)
        // 但是有个新的问题:同时创建同一个类的两个不同实例(类参数不同),是无法实现的 0.0  或许我们需要借助一个新容器?那么这样做岂非太麻烦了?
        $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
        $this->_params[$class] = $params;
        unset($this->_singletons[$class]);
        return $this;
    }

    public function setSingleton($class, $definition = [], array $params = [])
    {
        // 这个方法和set()方法差不多,唯一的区别就是,使用set()方法注册的类,每次get()的时候,都是重新生成一个新对象。
        // 而setSingleton()方法永远存储第一次生成的对象,并将其保存在 $this->_singletons属性中。每次调用get()方法,都是获取第一次生成的实例。
        $this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
        $this->_params[$class] = $params;
        $this->_singletons[$class] = null; // 保存键,get()方法中会根据array_key_exists()来判断
        return $this;
    }

    public function has($class)
    {
        return isset($this->_definitions[$class]);
    }

    public function hasSingleton($class, $checkInstance = false)
    {
        // 是否存在实例,第二个参数表示是否已经实例化。
        return $checkInstance ? isset($this->_singletons[$class]) : array_key_exists($class, $this->_singletons);
    }


    public function clear($class)
    {
        // 移除
        unset($this->_definitions[$class], $this->_singletons[$class]);
    }

  
    protected function normalizeDefinition($class, $definition)
    {
        // 将类的定义规范化。

        // $definition为空,返回类名称就是入参$class。注意类名要包含命名空间哦。
        if (empty($definition)) {
            return ['class' => $class];
        } elseif (is_string($definition)) {
            // $definition是字符串,则就是完整的正确的类名称。(包含命名空间的)
            return ['class' => $definition];
        } elseif ($definition instanceof Instance) {
            return ['class' => $definition->id];
        } elseif (is_callable($definition, true) || is_object($definition)) {
            return $definition;
        } elseif (is_array($definition)) {
            if (!isset($definition['class']) && isset($definition['__class'])) {
                $definition['class'] = $definition['__class'];
                unset($definition['__class']);
            }
            if (!isset($definition['class'])) {
                if (strpos($class, '\\') !== false) {
                    $definition['class'] = $class;
                } else {
                    throw new InvalidConfigException('A class definition requires a "class" member.');
                }
            }

            return $definition;
        }

        throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
    }


    public function getDefinitions()
    {
        return $this->_definitions;
    }

    protected function build($class, $params, $config)
    {
        // 整个容器最关键的方法:创建一个类的实例。发现类的依赖关系,解决依赖。实例化依赖的相关类,并注入。

        /* @var $reflection ReflectionClass */

        // 这里只是获取需要生成实例的参数
        list($reflection, $dependencies) = $this->getDependencies($class);

        // 配置中存在“__construct()”键,覆盖实例化的初始值。
        if (isset($config['__construct()'])) {
            foreach ($config['__construct()'] as $index => $param) {
                $dependencies[$index] = $param;
            }
            unset($config['__construct()']);
        }

        // 参数中存在值,覆盖实例化的初始值。
        foreach ($params as $index => $param) {
            $dependencies[$index] = $param;
        }

        // 这里处理准备生成实例的参数。如果参数是对象,则获取实际需要的具体对象。
        $dependencies = $this->resolveDependencies($dependencies, $reflection);
        if (!$reflection->isInstantiable()) {
            throw new NotInstantiableException($reflection->name);
        }
        if (empty($config)) {
            // 根据参数生成新的实例
            return $reflection->newInstanceArgs($dependencies);
        }

        $config = $this->resolveDependencies($config);

        if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
            // set $config as the last parameter (existing one will be overwritten)
            // 实现了接口:yii\base\Configurable,则将配置设置为最后一个参数(覆盖写),然后生成实例。
            $dependencies[count($dependencies) - 1] = $config;
            return $reflection->newInstanceArgs($dependencies);
        }

        // 其他情况,$config则是生成实例类中的属性。
        $object = $reflection->newInstanceArgs($dependencies);
        foreach ($config as $name => $value) {
            $object->$name = $value;
        }

        return $object;
    }


    protected function mergeParams($class, $params)
    {
        // 这个方法主要是将类的构造方法中的参数和用户传入的参数合并。

        // $this->_params是容器中所有类的构造方法中参数的全局注册树。
        if (empty($this->_params[$class])) {
            return $params;
        } elseif (empty($params)) {
            return $this->_params[$class];
        }

        $ps = $this->_params[$class];
        foreach ($params as $index => $value) {
            $ps[$index] = $value;
        }

        return $ps;
    }

    protected function getDependencies($class)
    {
        // 这个方法返回了类的所有依赖。
        // 返回的第一个参数是当前类的反射类,第二个参数是相关依赖。

        // 如果已经存在当前类的反射,就直接返回
        if (isset($this->_reflections[$class])) {
            return [$this->_reflections[$class], $this->_dependencies[$class]];
        }

        $dependencies = [];
        try {
            $reflection = new ReflectionClass($class);
        } catch (\ReflectionException $e) {
            throw new InvalidConfigException('Failed to instantiate component or class "' . $class . '".', 0, $e);
        }

        $constructor = $reflection->getConstructor();
        if ($constructor !== null) {
            foreach ($constructor->getParameters() as $param) {
                if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
                    // php版本大于等于5.6,并且参数是可变的,直接返回。(可变参数是最后一个参数。)
                    break;
                } elseif ($param->isDefaultValueAvailable()) {
                    // 默认值可用,就将默认值存储。
                    $dependencies[] = $param->getDefaultValue();
                } else {
                    $c = $param->getClass();
                    // 没有默认值,获取当前类名称,生成instance实例
                    $dependencies[] = Instance::of($c === null ? null : $c->getName());
                }
            }
        }

        // 赋值并返回。
        $this->_reflections[$class] = $reflection;
        $this->_dependencies[$class] = $dependencies;

        return [$reflection, $dependencies];
    }

    protected function resolveDependencies($dependencies, $reflection = null)
    {
        // 这个方法的作用,就是将目标类中的构造方法中,依赖类的参数,替换为实际的对象实例。
        // 如果当前类不属于引用对象类instance,那么就不做处理。
        foreach ($dependencies as $index => $dependency) {
            if ($dependency instanceof Instance) {
                if ($dependency->id !== null) {
                    // 首先,如果存在id属性,那么就继续从容器中获取依赖类。
                    $dependencies[$index] = $this->get($dependency->id);
                } elseif ($reflection !== null) {
                    // 如果存在反射类,那么就通过反射,获取构造函数类,再获取构造函数参数,再通过索引获取参数名称。
                    $name = $reflection->getConstructor()->getParameters()[$index]->getName();
                    $class = $reflection->getName(); // 获取类名称
                    // 剖出类缺少参数的异常
                    throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
                }
            }
        }

        return $dependencies;
    }

    public function invoke(callable $callback, $params = [])
    {
        // 解析函数参数中的依赖,然后调用回调函数。
        return call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
    }


    public function resolveCallableDependencies(callable $callback, $params = [])
    {
        if (is_array($callback)) {
            $reflection = new \ReflectionMethod($callback[0], $callback[1]);
        } elseif (is_object($callback) && !$callback instanceof \Closure) {
            $reflection = new \ReflectionMethod($callback, '__invoke');
        } else {
            $reflection = new \ReflectionFunction($callback);
        }

        $args = [];

        // 是否是关联数组
        $associative = ArrayHelper::isAssociative($params);

        foreach ($reflection->getParameters() as $param) {
            $name = $param->getName();
            if (($class = $param->getClass()) !== null) {
                $className = $class->getName();
                if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
                    // 参数可变,合并参数。并打断。
                    $args = array_merge($args, array_values($params));
                    break;
                } elseif ($associative && isset($params[$name]) && $params[$name] instanceof $className) {
                    $args[] = $params[$name];
                    unset($params[$name]);
                } elseif (!$associative && isset($params[0]) && $params[0] instanceof $className) {
                    $args[] = array_shift($params);
                } elseif (isset(Yii::$app) && Yii::$app->has($name) && ($obj = Yii::$app->get($name)) instanceof $className) {
                    $args[] = $obj;
                } else {
                    // If the argument is optional we catch not instantiable exceptions
                    try {
                        $args[] = $this->get($className);
                    } catch (NotInstantiableException $e) {
                        if ($param->isDefaultValueAvailable()) {
                            $args[] = $param->getDefaultValue();
                        } else {
                            throw $e;
                        }
                    }
                }
            } elseif ($associative && isset($params[$name])) {
                $args[] = $params[$name];
                unset($params[$name]);
            } elseif (!$associative && count($params)) {
                $args[] = array_shift($params);
            } elseif ($param->isDefaultValueAvailable()) {
                $args[] = $param->getDefaultValue();
            } elseif (!$param->isOptional()) {
                $funcName = $reflection->getName();
                throw new InvalidConfigException("Missing required parameter \"$name\" when calling \"$funcName\".");
            }
        }

        foreach ($params as $value) {
            $args[] = $value;
        }

        return $args;
    }

    public function setDefinitions(array $definitions)
    {
        // 实现了 set() 的队列。
        foreach ($definitions as $class => $definition) {
            if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition && is_array($definition[1])) {
                $this->set($class, $definition[0], $definition[1]);
                continue;
            }

            $this->set($class, $definition);
        }
    }


    public function setSingletons(array $singletons)
    {
        // 实现了 setSingleton() 的队列。
        foreach ($singletons as $class => $definition) {
            if (is_array($definition) && count($definition) === 2 && array_values($definition) === $definition) {
                $this->setSingleton($class, $definition[0], $definition[1]);
                continue;
            }

            $this->setSingleton($class, $definition);
        }
    }
}

上边我贴出了完整的容器类以及相关的源码分析。其中比较重要的几个方法是:get(),set(),build()。

真正的实例的创建的方法是build(),而它是在get()方法中进行调用。set()方法只是设置需要实例化它的参数。

上一篇:游戏中遮挡剔除方案总结


下一篇:C# 反射(Reflection)的理解和使用