读本篇文章,建议先看看我之前的文章php依赖注入
到此,现在我们正式开始分析yii2框架组件构造流程
我们先从yii\di\ServiceLocator(服务定位器)入手吧!!让我们先看个实例:
use yii\di\ServiceLocator;
use yii\caching\FileCache;
$locator = new ServiceLocator;
// 通过一个可用于创建该组件的类名,注册 "cache" (缓存)组件。
$locator->set('cache', 'yii\caching\ApcCache');
// 通过一个可用于创建该组件的配置数组,注册 "db" (数据库)组件。
$locator->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=localhost;dbname=demo',
'username' => 'root',
'password' => '',
]);
我们直接打开ServiceLocator的set方法:
//这个函数很简单,就是定义一个id对应$definition,并保存到$this->_definitions数组中
public function set($id, $definition)
{
if ($definition === null) {
unset($this->_components[$id], $this->_definitions[$id]);
return;
}
unset($this->_components[$id]);
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));
}
}
现在我们有set方法,如果我们需要指定类名的对象呢,这时候需要看看ServiceLocator的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 {
//Yii::createObject创建对象实例(重点方法)
return $this->_components[$id] = Yii::createObject($definition);
}
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
} else {
return null;
}
}
跟踪Yii::createObject方法
public static function createObject($type, array $params = [])
{
if (is_string($type)) {
//static::$container就是yii\di\Container对象(依赖注入容器)
return static::$container->get($type, $params);
} elseif (is_array($type) && isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
} elseif (is_callable($type, true)) {
return call_user_func($type, $params);
} elseif (is_array($type)) {
throw new InvalidConfigException('Object configuration must be an array containing a "class" element.');
} else {
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
}
我们来看看yii2的依赖注入容器怎么实现的,打开yii\di\Container的get方法:
public function get($class, $params = [], $config = [])
{
//$this->_singletons保存单例对象
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {
//通过php反射机制实现$class的实例化
return $this->build($class, $params, $config);
}
//代表已经使用过set方法定义过$class
$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));
}
if (array_key_exists($class, $this->_singletons)) {
// 单例对象
$this->_singletons[$class] = $object;
}
return $object;
}
我们打开这个$this->build方法:
protected function build($class, $params, $config)
{
/*
$reflection为反射类
$denpendencies为$class类构造函数参数,如果构造函数参数里面有依赖别的类,举例:
[new Instance(id属性=该类名),new Instance(id属性=该类名)]
*/
list ($reflection, $dependencies) = $this->getDependencies($class);
//将$params的key和值加到$class构造函数参数数组列表中
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
//将构造函数参数中有yii\di\Instance的id属性不为空,则将继续递归调用$this->get将所依赖的类实例化
$dependencies = $this->resolveDependencies($dependencies, $reflection);
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// set $config as the last parameter (existing one will be overwritten)
$dependencies[count($dependencies) - 1] = $config;
return $reflection->newInstanceArgs($dependencies);
} else {
$object = $reflection->newInstanceArgs($dependencies);
//将$config数组赋值到实例化对象的属性上
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
继续追$this->getDependencies方法:
//得到[$reflection, $dependencies];
protected function getDependencies($class)
{
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
$reflection = new ReflectionClass($class);
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
if ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
} else {
$c = $param->getClass();
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
//缓存$reflection反射类,以及$class类构造函数参数数组
$this->_reflections[$class] = $reflection;
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
我们打开$this->resolveDependencies方法:
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
$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;
}
最后来个yii\di\Instance类的具体内容(跟我之前那个类代码基本一样,多了个ensure方法):
namespace yii\di;
use Yii;
use yii\base\InvalidConfigException;
class Instance
{
public $id;
protected function __construct($id)
{
$this->id = $id;
}
public static function of($id)
{
return new static($id);
}
/**
*
* ```php
* use yii\db\Connection;
*
* // returns Yii::$app->db
* $db = Instance::ensure('db', Connection::className());
* // returns an instance of Connection using the given configuration
* $db = Instance::ensure(['dsn' => 'sqlite:path/to/my.db'], Connection::className());
* ```
*/
public static function ensure($reference, $type = null, $container = null)
{
if (is_array($reference)) {
$class = isset($reference['class']) ? $reference['class'] : $type;
if (!$container instanceof Container) {
$container = Yii::$container;
}
unset($reference['class']);
return $container->get($class, [], $reference);
} elseif (empty($reference)) {
throw new InvalidConfigException('The required component is not specified.');
}
if (is_string($reference)) {
$reference = new static($reference);
} elseif ($type === null || $reference instanceof $type) {
return $reference;
}
if ($reference instanceof self) {
$component = $reference->get($container);
if ($type === null || $component instanceof $type) {
return $component;
} else {
throw new InvalidConfigException('"' . $reference->id . '" refers to a ' . get_class($component) . " component. $type is expected.");
}
}
$valueType = is_object($reference) ? get_class($reference) : gettype($reference);
throw new InvalidConfigException("Invalid data type: $valueType. $type is expected.");
}
public function get($container = null)
{
if ($container) {
return $container->get($this->id);
}
if (Yii::$app && Yii::$app->has($this->id)) {
return Yii::$app->get($this->id);
} else {
return Yii::$container->get($this->id);
}
}
}