正文
实例引导
看下面代码:
namespace app\models;
use yii\base\Object;
use yii\db\Connection;
// 定义接口
interface UserFinderInterface
{
function findUser();
}
// 定义类,实现接口
class UserFinder extends Object implements UserFinderInterface
{
public $db;
// 从构造函数看,这个类依赖于 Connection
public function __construct(Connection $db, $config = [])
{
$this->db = $db;
parent::__construct($config);
}
public function findUser()
{
}
}
class UserLister extends Object
{
public $finder;
// 从构造函数看,这个类依赖于 UserFinderInterface接口
public function __construct(UserFinderInterface $finder, $config = [])
{
$this->finder = $finder;
parent::__construct($config);
}
}
从依赖关系看,这里的 UserLister 类依赖于接口 UserFinderInterface , 而接口有一个实现就是 UserFinder 类,但这类又依赖于 Connection 。
那么,按照一般常规的作法,要实例化一个 UserLister 通常这么做:
$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);
就是逆着依赖关系,从最底层的 Connection 开始实例化,接着是 UserFinder 最后是 UserLister 。 在写代码的时候,这个前后顺序是不能乱的。而且,需要用到的单元,你要自己一个一个提前准备好。 如果是多人开发,如果依赖有10-20个,估计光实例化的代码,就可以写满一屏幕了。你自己都被搞乱了,所以我们需要一个解决依赖加载的方案。
而且,如果是团队开发,有些单元应当是共用的,如邮件投递服务。 不能说你写个模块,要用到邮件服务了,就自己实例化一个邮件服务吧? 那样岂不是有N模块就有N个邮件服务了? 最好的方式是使邮件服务成为一个单例,这样任何模块在需要邮件服务时,使用的其实是同一个实例。 用传统的这种实例化对象的方法来实现的话,就没那么直接了。
如果改用DI容器的话,应该是这样的:
use yii\di\Container;
// 创建一个DI容器
$container = new Container;
// 为Connection指定一个数组作为依赖,当需要Connection的实例时,使用这个数组进行创建
$container->set('yii\db\Connection', [
'dsn' => '...',
]);
// 在需要使用接口 UserFinderInterface 时,采用UserFinder类实现
$container->set('app\models\UserFinderInterface', [
'class' => 'app\models\UserFinder',
]);
// 为UserLister定义一个别名
$container->set('userLister', 'app\models\UserLister');
// 获取这个UserList的实例
$lister = $container->get('userLister');
采用DI容器的办法,首先各 set() 语句没有前后关系的要求, set() 只是写入特定的数据结构, 并未涉及具体依赖关系的解析。 所以,前后关系不重要,先定义什么依赖,后定义什么依赖没有关系。
其次,上面根本没有在DI容器中定义 UserFinder 对于 Connection 的依赖。 但是DI容器通过对 UserFinder 构造函数的分析, 能了解到这个类会对 Connection 依赖。这个过程是自动的。
最后,上面只有一个 get() 看起来好像根本没有实例化其他单元一样,如 Connection。但事实上,DI容器已经安排好了一切。 在获取 userLister 之前, Connection 和 UserFinder 都会被自动实例化。 其中, Connection 是根据依赖定义中的配置数组进行实例化的。
有关概念
在了解Service Locator 和 Dependency Injection 之前,有必要先来了解一些高大上的概念。
- 依赖倒置原则(Dependence Inversion Principle, DIP)
DIP是一种软件设计的指导思想。传统软件设计中,上层代码依赖于下层代码,当下层出现变动时, 上层代码也要相应变化,维护成本较高。 而DIP的核心思想是上层定义接口,下层实现这个接口, 从而使得下层依赖于上层,降低耦合度,提高整个系统的弹性。这是一种经实践证明的有效策略。
- 控制反转(Inversion of Control, IoC)
IoC就是DIP的一种具体思路,DIP只是一种理念、思想,而IoC是一种实现DIP的方法。
IoC的核心是将类(上层)所依赖的单元(下层)的实例化过程交由第三方来实现。 一个简单的特征,
就是类中不对所依赖的单元有诸如 $component = new yii\component\SomeClass()
的实例化语句。
- 依赖注入(Dependence Injection, DI)
DI是IoC的一种设计模式,是一种套路,按照DI的套路,就可以实现IoC,就能符合DIP原则。 DI的核心是把类所依赖的单元的实例化过程,放到类的外面去实现。
- 控制反转容器(IoC Container)
当项目比较大时,依赖关系可能会很复杂。 而IoC Container提供了动态地创建、注入依赖单元,映射依赖关系等功能,减少了许多代码量。
Yii 设计了一个 yii\di\Container
来实现了 DI Container。
- 服务定位器(Service Locator)
Service Locator是IoC的另一种实现方式, 其核心是把所有可能用到的依赖单元交由Service Locator进行实例化和创建、配置, 把类对依赖单元的依赖,转换成类对Service Locator的依赖。 DI 与 Service Locator并不冲突,两者可以结合使用。 目前,Yii2.0把这DI和Service Locator这两个东西结合起来使用,或者说通过DI容器,实现了Service Locator。
依赖注入
首先讲讲DI。在Web应用中,很常见的是使用各种第三方Web Service实现特定的功能,比如发送邮件、推送微博等。 假设要实现当访客在博客上发表评论后,向博文的作者发送Email的功能,通常代码会是这样:
// 为邮件服务定义抽象层
interface EmailSenderInterface
{
public function send(...);
}
// 定义Gmail邮件服务
class GmailSender implements EmailSenderInterface
{
...
// 实现发送邮件的类方法
public function send(...)
{
...
}
}
// 定义评论类
class Comment extend yii\db\ActiveRecord
{
// 用于引用发送邮件的库
private $_eMailSender;
// 初始化时,实例化 $_eMailSender
public function init()
{
...
// 这里假设使用Gmail的邮件服务
$this->_eMailSender = GmailSender::getInstance();
...
}
// 当有新的评价,即 save() 方法被调用之后中,会触发以下方法
public function afterInsert()
{
...
//
$this->_eMailSender->send(...);
...
}
}
那么这种常见的设计方法有什么问题呢? 主要问题在于 Comment 对于 GmailSender 的依赖(对于EmailSenderInterface的依赖不可避免), 假设有一天突然不使用Gmail提供的服务了,改用QQ或自建的邮件服务了。 那么,你不得不修改 Comment::init() 里面对 $_eMailSender 的实例化语句:
$this->_eMailSender = MyEmailSender::getInstance();
这个问题的本质在于,你今天写完这个Comment,只能用于这个项目,哪天你开发别的项目要实现类似的功能, 你还要针对新项目使用的邮件服务修改这个Comment。代码的复用性不高呀。 有什么办法可以不改变Comment的代码,就能扩展成对各种邮件服务都支持么? 换句话说,有办法将Comment和GmailSender解耦么?有办法提高Comment的普适性、复用性么?
依赖注入就是为了解决这个问题而生的,当然,DI也不是唯一解决问题的办法,毕竟条条大路通罗马。 Service Locator也是可以实现解耦的。
在Yii中使用DI解耦,有2种注入方式:构造函数注入、属性注入。
构造函数注入
构造函数注入通过构造函数的形参,为类内部的抽象单元提供实例化。 具体的构造函数调用代码,由外部代码决定。具体例子如下:
// 这是构造函数注入的例子
class Comment extend yii\db\ActiveRecord
{
// 用于引用发送邮件的库
private $_eMailSender;
// 构造函数注入
public function __construct($emailSender)
{
...
$this->_eMailSender = $emailSender;
...
}
// 当有新的评价,即 save() 方法被调用之后中,会触发以下方法
public function afterInsert()
{
...
//
$this->_eMailSender->send(...);
...
}
}
// 实例化两种不同的邮件服务,当然,他们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();
// 用构造函数将GmailSender注入
$comment1 = new Comment(sender1);
// 使用Gmail发送邮件
$comment1.save();
// 用构造函数将MyEmailSender注入
$comment2 = new Comment(sender2);
// 使用MyEmailSender发送邮件
$comment2.save();
上面的代码对比原来的代码,解决了Comment类对于GmailSender等具体类的依赖,通过构造函数, 将相应的实现了 EmailSenderInterface接口的类实例传入Comment类中,使得Comment类可以适用于不同的邮件服务。 从此以后,无论要使用何何种邮件服务,只需写出新的EmailSenderInterface实现即可, Comment类的代码不再需要作任何更改,扩展起来、测试起来都省心省力。
属性注入
与构造函数注入类似,属性注入通过setter或public成员变量,将所依赖的单元注入到类内部。 具体的属性写入,由外部代码决定。具体例子如下:
// 这是属性注入的例子
class Comment extend yii\db\ActiveRecord
{
// 用于引用发送邮件的库
private $_eMailSender;
// 定义了一个 setter()
public function setEmailSender($value)
{
$this->_eMailSender = $value;
}
// 当有新的评价,即 save() 方法被调用之后中,会触发以下方法
public function afterInsert()
{
...
//
$this->_eMailSender->send(...);
...
}
}
// 实例化两种不同的邮件服务,当然,他们都实现了EmailSenderInterface
sender1 = new GmailSender();
sender2 = new MyEmailSender();
$comment1 = new Comment;
// 使用属性注入
$comment1->eMailSender = sender1;
// 使用Gmail发送邮件
$comment1.save();
$comment2 = new Comment;
// 使用属性注入
$comment2->eMailSender = sender2;
// 使用MyEmailSender发送邮件
$comment2.save();
上面的Comment如果将 private $_eMailSender
改成 public $eMailSender
并删除 setter函数, 也是可以达到同样的效果的。
与构造函数注入类似,属性注入也是将Comment类所依赖的EmailSenderInterface的实例化过程放在Comment类以外。 这就是依赖注入的本质所在。 为什么称为注入?从外面把东西打进去,就是注入。什么是外,什么是内? 要解除依赖的类内部就是内,实例化所依赖单元的地方就是外。
DI容器
从上面DI两种注入方式来看,依赖单元的实例化代码是一个重复、繁琐的过程。 可以想像,一个Web应用的某一组件会依赖于若干单元, 这些单元又有可能依赖于更低层级的单元, 从而形成依赖嵌套的情形。那么,这些依赖单元的实例化、注入过程的代码可能会比较长, 前后关系也需要特别地注意, 必须将被依赖的放在需要注入依赖的前面进行实例化。
为了解决这一的问题,DI容器被设计出来了。 他知道如何对对象及对象的所有依赖,和这些依赖的依赖,进行实例化和配置。
DI容器中的内容
DI容器中实例的表示
容器顾名思义是用来装东西的,DI容器里面的东西是什么呢?Yii使用 yii\di\Instance
来表示容器中的东西。
当然Yii中还将这个类用于Service Locator,这个在讲Service Locator时再具体谈谈。
yii\di\Instance
本质上是DI容器中对于某一个类实例的引用,它的代码:
<?php
namespace yii\di;
use Yii;
use yii\base\InvalidConfigException;
/**
* Instance represents a reference to a named object in a dependency injection (DI) container or a service locator.
*
* You may use [[get()]] to obtain the actual object referenced by [[id]].
*
* Instance is mainly used in two places:
*
* - When configuring a dependency injection container, you use Instance to reference a class name, interface name
* or alias name. The reference can later be resolved into the actual object by the container.
* - In classes which use service locator to obtain dependent objects.
*
* The following example shows how to configure a DI container with Instance:
*
* $container = new \yii\di\Container;
* $container->set('cache', [
* 'class' => 'yii\caching\DbCache',
* 'db' => Instance::of('db')
* ]);
* $container->set('db', [
* 'class' => 'yii\db\Connection',
* 'dsn' => 'sqlite:path/to/file.db',
* ]);
*
* And the following example shows how a class retrieves a component from a service locator:
*
* class DbCache extends Cache
* {
* public $db = 'db';
*
* public function init()
* {
* parent::init();
* $this->db = Instance::ensure($this->db, 'yii\db\Connection');
* }
* }
*/
class Instance
{
/**
* 仅有的属性,用于保存类名、接口名或者别名
* @var string the component ID, class name, interface name or alias name
*/
public $id;
/**
* 构造函数,仅将传入的ID赋值给 $id 属性
* Constructor.
* @param string $id the component ID
*/
protected function __construct($id)
{
$this->id = $id;
}
/**
* 静态方法创建一个Instance实例
* Creates a new Instance object.
* @param string $id the component ID
* @return Instance the new Instance object.
*/
public static function of($id)
{
return new static($id);
}
/**
* 静态方法,用于将引用解析成实际的对象,并确保这个对象的类型
* Resolves the specified reference into the actual object and makes sure it is of the specified type.
*
* The reference may be specified as a string or an Instance object. If the former,
* it will be treated as a component ID, a class/interface name or an alias, depending on the container type.
*
* If you do not specify a container, the method will first try `Yii::$app` followed by `Yii::$container`.
*
* For example,
*
* 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());
*
* @param object|string|array|static $reference an object or a reference to the desired object.
* You may specify a reference in terms of a component ID or an Instance object.
* Starting from version 2.0.2, you may also pass in a configuration array for creating the object.
* If the "class" value is not specified in the configuration array, it will use the value of `$type`.
* @param string $type the class/interface name to be checked. If null, type check will not be performed.
* @param ServiceLocator|Container $container the container. This will be passed to [[get()]].
* @return object the object referenced by the Instance, or `$reference` itself if it is an object.
* @throws InvalidConfigException if the reference is invalid
*/
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) {
try {
$component = $reference->get($container);
} catch(\ReflectionException $e) {
throw new InvalidConfigException('Failed to instantiate component or class "' . $reference->id . '".', 0, $e);
}
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.");
}
/**
* 获取这个实例所引用的实际对象,事实上它调用的是 yii\di\Container::get()来获取实际对象
* Returns the actual object referenced by this Instance object.
* @param ServiceLocator|Container $container the container used to locate the referenced object.
* If null, the method will first try `Yii::$app` then `Yii::$container`.
* @return object the actual object referenced by this Instance object.
*/
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);
}
}
}
对于 yii\di\Instance
:
- 表示的是容器中的内容,代表的是对于实际对象的引用。
- DI容器可以通过他获取所引用的实际对象。
- 类仅有的一个属性 id 一般表示的是实例的类型。
DI容器的数据结构
看一下 yii\di\Container
代码:
<?php
namespace yii\di;
use ReflectionClass;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
*
* A dependency injection (DI) container is an object that knows how to instantiate and configure objects and
* all their dependent objects. For more information about DI, please refer to
* [Martin Fowler's article](http://martinfowler.com/articles/injection.html).
*
* Container supports constructor injection as well as property injection.
*
* To use Container, you first need to set up the class dependencies by calling [[set()]].
* You then call [[get()]] to create a new class object. Container will automatically instantiate
* dependent objects, inject them into the object being created, configure and finally return the newly created object.
*
* By default, [[\Yii::$container]] refers to a Container instance which is used by [[\Yii::createObject()]]
* to create new object instances. You may use this method to replace the `new` operator
* when creating a new object, which gives you the benefit of automatic dependency resolution and default
* property configuration.
*
* Below is an example of using Container:
*
* ---php
* namespace app\models;
*
* use yii\base\Object;
* use yii\db\Connection;
* use yii\di\Container;
*
* interface UserFinderInterface
* {
* function findUser();
* }
*
* class UserFinder extends Object implements UserFinderInterface
* {
* public $db;
*
* public function __construct(Connection $db, $config = [])
* {
* $this->db = $db;
* parent::__construct($config);
* }
*
* public function findUser()
* {
* }
* }
*
* class UserLister extends Object
* {
* public $finder;
*
* public function __construct(UserFinderInterface $finder, $config = [])
* {
* $this->finder = $finder;
* parent::__construct($config);
* }
* }
*
* $container = new Container;
* $container->set('yii\db\Connection', [
* 'dsn' => '...',
* ]);
* $container->set('app\models\UserFinderInterface', [
* 'class' => 'app\models\UserFinder',
* ]);
* $container->set('userLister', 'app\models\UserLister');
*
* $lister = $container->get('userLister');
*
* // which is equivalent to:
*
* $db = new \yii\db\Connection(['dsn' => '...']);
* $finder = new UserFinder($db);
* $lister = new UserLister($finder);
* ---
*
* For more details and usage information on Container, see the [guide article on di-containers](guide:concept-di-container).
*
* @property array $definitions The list of the object definitions or the loaded shared objects (type or ID =>
* definition or instance). This property is read-only.
*
*/
class Container extends Component
{
/**
* 用于保存单例Singleton对象,以对象类型为键
* @var array singleton objects indexed by their types
*/
private $_singletons = [];
/**
* 用于保存依赖的定义,以对象类型为键
* @var array object definitions indexed by their types
*/
private $_definitions = [];
/**
* 用于保存构造函数的参数,以对象类型为键
* @var array constructor parameters indexed by object types
*/
private $_params = [];
/**
* 用于缓存ReflectionClass对象,以类名或接口名为键
* @var array cached ReflectionClass objects indexed by class/interface names
*/
private $_reflections = [];
/**
* 用于缓存依赖信息,以类名或接口名为键
* @var array cached dependencies indexed by class/interface names. Each class name
* is associated with a list of constructor parameter types or default values.
*/
private $_dependencies = [];
/**
* Returns an instance of the requested class.
*
* You may provide constructor parameters (`$params`) and object configurations (`$config`)
* that will be used during the creation of the instance.
*
* If the class implements [[\yii\base\Configurable]], the `$config` parameter will be passed as the last
* parameter to the class constructor; Otherwise, the configuration will be applied *after* the object is
* instantiated.
*
* Note that if the class is declared to be singleton by calling [[setSingleton()]],
* the same instance of the class will be returned each time this method is called.
* In this case, the constructor parameters and object configurations will be used
* only if the class is instantiated the first time.
*
* @param string $class the class name or an alias name (e.g. `foo`) that was previously registered via [[set()]]
* or [[setSingleton()]].
* @param array $params a list of constructor parameter values. The parameters should be provided in the order
* they appear in the constructor declaration. If you want to skip some parameters, you should index the remaining
* ones with the integers that represent their positions in the constructor parameter list.
* @param array $config a list of name-value pairs that will be used to initialize the object properties.
* @return object an instance of the requested class.
* @throws InvalidConfigException if the class cannot be recognized or correspond to an invalid definition
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
*/
public function get($class, $params = [], $config = [])
{
// 已经有一个完成实例化的单例,直接引用这个单例
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
// 是个尚未注册过的依赖,说明它不依赖其他单元,或者依赖信息不用定义,则根据传入的参数创建一个实例
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}
// 注意这里创建了 $_definitions[$class] 数组的副本
$definition = $this->_definitions[$class];
// 依赖的定义是个 PHP callable,调用之
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) {
// 这是递归终止的重要条件,$definition中定义的class与自己get的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)) {
// singleton
$this->_singletons[$class] = $object;
}
return $object;
}
/**
* Registers a class definition with this container.
*
* For example,
*
* ---php
* // register a class name as is. This can be skipped.
* $container->set('yii\db\Connection');
*
* // register an interface
* // When a class depends on the interface, the corresponding class
* // will be instantiated as the dependent object
* $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
*
* // register an alias name. You can use $container->get('foo')
* // to create an instance of Connection
* $container->set('foo', 'yii\db\Connection');
*
* // register a class with configuration. The configuration
* // will be applied when the class is instantiated by get()
* $container->set('yii\db\Connection', [
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
* 'username' => 'root',
* 'password' => '',
* 'charset' => 'utf8',
* ]);
*
* // register an alias name with class configuration
* // In this case, a "class" element is required to specify the class
* $container->set('db', [
* 'class' => 'yii\db\Connection',
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
* 'username' => 'root',
* 'password' => '',
* 'charset' => 'utf8',
* ]);
*
* // register a PHP callable
* // The callable will be executed when $container->get('db') is called
* $container->set('db', function ($container, $params, $config) {
* return new \yii\db\Connection($config);
* });
* ---
*
* If a class definition with the same name already exists, it will be overwritten with the new one.
* You may use [[has()]] to check if a class definition already exists.
*
* @param string $class class name, interface name or alias name
* @param mixed $definition the definition associated with `$class`. It can be one of the following:
*
* - a PHP callable: The callable will be executed when [[get()]] is invoked. The signature of the callable
* should be `function ($container, $params, $config)`, where `$params` stands for the list of constructor
* parameters, `$config` the object configuration, and `$container` the container object. The return value
* of the callable will be returned by [[get()]] as the object instance requested.
* - a configuration array: the array contains name-value pairs that will be used to initialize the property
* values of the newly created object when [[get()]] is called. The `class` element stands for the
* the class of the object to be created. If `class` is not specified, `$class` will be used as the class name.
* - a string: a class name, an interface name or an alias name.
* @param array $params the list of constructor parameters. The parameters will be passed to the class
* constructor when [[get()]] is called.
* @return $this the container itself
*/
public function set($class, $definition = [], array $params = [])
{
// 规范化 $definition 并写入 $_definitions[$class]
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
// 将构造函数参数写入 $_params[$class]
$this->_params[$class] = $params;
// 删除$_singletons[$class]
unset($this->_singletons[$class]);
return $this;
}
/**
* Registers a class definition with this container and marks the class as a singleton class.
*
* This method is similar to [[set()]] except that classes registered via this method will only have one
* instance. Each time [[get()]] is called, the same instance of the specified class will be returned.
*
* @param string $class class name, interface name or alias name
* @param mixed $definition the definition associated with `$class`. See [[set()]] for more details.
* @param array $params the list of constructor parameters. The parameters will be passed to the class
* constructor when [[get()]] is called.
* @return $this the container itself
* @see set()
*/
public function setSingleton($class, $definition = [], array $params = [])
{
// 规范化 $definition 并写入 $_definitions[$class]
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
// 将构造函数参数写入 $_params[$class]
$this->_params[$class] = $params;
// 将$_singleton[$class]置为null,表示还未实例化
$this->_singletons[$class] = null;
return $this;
}
/**
* Returns a value indicating whether the container has the definition of the specified name.
* @param string $class class name, interface name or alias name
* @return bool whether the container has the definition of the specified name..
* @see set()
*/
public function has($class)
{
return isset($this->_definitions[$class]);
}
/**
* Returns a value indicating whether the given name corresponds to a registered singleton.
* @param string $class class name, interface name or alias name
* @param bool $checkInstance whether to check if the singleton has been instantiated.
* @return bool whether the given name corresponds to a registered singleton. If `$checkInstance` is true,
* the method should return a value indicating whether the singleton has been instantiated.
*/
public function hasSingleton($class, $checkInstance = false)
{
return $checkInstance ? isset($this->_singletons[$class]) : array_key_exists($class, $this->_singletons);
}
/**
* Removes the definition for the specified name.
* @param string $class class name, interface name or alias name
*/
public function clear($class)
{
unset($this->_definitions[$class], $this->_singletons[$class]);
}
/**
* Normalizes the class definition.
* @param string $class class name
* @param string|array|callable $definition the class definition
* @return array the normalized class definition
* @throws InvalidConfigException if the definition is invalid.
*/
protected function normalizeDefinition($class, $definition)
{
// $definition 是空的转换成 ['class' => $class] 形式
if (empty($definition)) {
return ['class' => $class];
// $definition 是字符串,转换成 ['class' => $definition] 形式
} elseif (is_string($definition)) {
return ['class' => $definition];
// $definition 是PHP callable 或对象,则直接将其作为依赖的定义
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
// $definition 是数组则确保该数组定义了 class 元素
} elseif (is_array($definition)) {
// 如果$definition中无class,则需要为其定义class
if (!isset($definition['class'])) {
// $class 中有 \ ,说明$class不是别名,而是类名,如 yii\db\Connection
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
} else {
throw new InvalidConfigException("A class definition requires a \"class\" member.");
}
}
// 返回定义了class的$definition
return $definition;
// 这也不是,那也不是,那就抛出异常算了
} else {
throw new InvalidConfigException("Unsupported definition type for \"$class\": " . gettype($definition));
}
}
/**
* Returns the list of the object definitions or the loaded shared objects.
* @return array the list of the object definitions or the loaded shared objects (type or ID => definition or instance).
*/
public function getDefinitions()
{
return $this->_definitions;
}
/**
* Creates an instance of the specified class.
* This method will resolve dependencies of the specified class, instantiate them, and inject
* them into the new instance of the specified class.
* @param string $class the class name
* @param array $params constructor parameters
* @param array $config configurations to be applied to the new instance
* @return object the newly created instance of the specified class
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
*/
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
list ($reflection, $dependencies) = $this->getDependencies($class);
// 用传入的 $params 的内容补充、覆盖到依赖信息中
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
$dependencies = $this->resolveDependencies($dependencies, $reflection);
// 如果$reflection不可实例化,则抛出异常
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
// 如果$config为空,则从给出的$dependencies参数创建一个新的类实例
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}
// $config不为空
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// 按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
// 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);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
/**
* 将用户指定的构造函数参数与通过[[set()]]注册的构造函数参数合并
* Merges the user-specified constructor parameters with the ones registered via [[set()]].
* @param string $class class name, interface name or alias name
* @param array $params the constructor parameters
* @return array the merged parameters
*/
protected function mergeParams($class, $params)
{
if (empty($this->_params[$class])) {
return $params;
} elseif (empty($params)) {
return $this->_params[$class];
} else {
$ps = $this->_params[$class];
foreach ($params as $index => $value) {
$ps[$index] = $value;
}
return $ps;
}
}
/**
* Returns the dependencies of the specified class.
* @param string $class class name, interface name or alias name
* @return array the dependencies of the specified class.
*/
protected function getDependencies($class)
{
// 如果已经缓存了其依赖信息,直接返回缓存中的依赖信息
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
// 使用PHP5 的反射机制来获取类的有关信息,主要就是为了获取依赖信息
$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();
// 构造函数没有默认值,则为其创建一个引用。 就是前面提到的 Instance 类型。
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
// 将 ReflectionClass 对象缓存起来
$this->_reflections[$class] = $reflection;
// 将依赖信息缓存起来
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
/**
* Resolves dependencies by replacing them with the actual object instances.
* @param array $dependencies the dependencies
* @param ReflectionClass $reflection the class reflection associated with the dependencies
* @return array the resolved dependencies
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
*/
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
// 前面getDependencies() 函数往 $_dependencies[] 中 写入的是一个 Instance 数组
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
// 向容器索要所依赖的实例,递归调用 yii\di\Container::get()
$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;
}
/**
* Invoke a callback with resolving dependencies in parameters.
*
* This methods allows invoking a callback and let type hinted parameter names to be
* resolved as objects of the Container. It additionally allow calling function using named parameters.
*
* For example, the following callback may be invoked using the Container to resolve the formatter dependency:
*
* ---php
* $formatString = function($string, \yii\i18n\Formatter $formatter) {
* // ...
* }
* Yii::$container->invoke($formatString, ['string' => 'Hello World!']);
* ---
*
* This will pass the string `'Hello World!'` as the first param, and a formatter instance created
* by the DI container as the second param to the callable.
*
* @param callable $callback callable to be invoked.
* @param array $params The array of parameters for the function.
* This can be either a list of parameters, or an associative array representing named function parameters.
* @return mixed the callback return value.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
* @since 2.0.7
*/
public function invoke(callable $callback, $params = [])
{
if (is_callable($callback)) {
return call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
} else {
return call_user_func_array($callback, $params);
}
}
/**
* Resolve dependencies for a function.
*
* This method can be used to implement similar functionality as provided by [[invoke()]] in other
* components.
*
* @param callable $callback callable to be invoked.
* @param array $params The array of parameters for the function, can be either numeric or associative.
* @return array The resolved dependencies.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
* @since 2.0.7
*/
public function resolveCallableDependencies(callable $callback, $params = [])
{
if (is_array($callback)) {
$reflection = new \ReflectionMethod($callback[0], $callback[1]);
} 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 ($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;
}
/**
* Registers class definitions within this container.
*
* @param array $definitions array of definitions. There are two allowed formats of array.
* The first format:
* - key: class name, interface name or alias name. The key will be passed to the [[set()]] method
* as a first argument `$class`.
* - value: the definition associated with `$class`. Possible values are described in
* [[set()]] documentation for the `$definition` parameter. Will be passed to the [[set()]] method
* as the second argument `$definition`.
*
* Example:
* ---php
* $container->setDefinitions([
* 'yii\web\Request' => 'app\components\Request',
* 'yii\web\Response' => [
* 'class' => 'app\components\Response',
* 'format' => 'json'
* ],
* 'foo\Bar' => function () {
* $qux = new Qux;
* $foo = new Foo($qux);
* return new Bar($foo);
* }
* ]);
* ---
*
* The second format:
* - key: class name, interface name or alias name. The key will be passed to the [[set()]] method
* as a first argument `$class`.
* - value: array of two elements. The first element will be passed the [[set()]] method as the
* second argument `$definition`, the second one — as `$params`.
*
* Example:
* ---php
* $container->setDefinitions([
* 'foo\Bar' => [
* ['class' => 'app\Bar'],
* [Instance::of('baz')]
* ]
* ]);
* ---
*
* @see set() to know more about possible values of definitions
* @since 2.0.11
*/
public function setDefinitions(array $definitions)
{
foreach ($definitions as $class => $definition) {
if (count($definition) === 2 && array_values($definition) === $definition) {
$this->set($class, $definition[0], $definition[1]);
continue;
}
$this->set($class, $definition);
}
}
/**
* Registers class definitions as singletons within this container by calling [[setSingleton()]]
*
* @param array $singletons array of singleton definitions. See [[setDefinitions()]]
* for allowed formats of array.
*
* @see setDefinitions() for allowed formats of $singletons parameter
* @see setSingleton() to know more about possible values of definitions
* @since 2.0.11
*/
public function setSingletons(array $singletons)
{
foreach ($singletons as $class => $definition) {
if (count($definition) === 2 && array_values($definition) === $definition) {
$this->setSingleton($class, $definition[0], $definition[1]);
continue;
}
$this->setSingleton($class, $definition);
}
}
}
在DI容器中,维护了5个数组,这是DI容器功能实现的基础:
// 用于保存单例Singleton对象,以对象类型为键
private $_singletons = [];
// 用于保存依赖的定义,以对象类型为键
private $_definitions = [];
// 用于保存构造函数的参数,以对象类型为键
private $_params = [];
// 用于缓存ReflectionClass对象,以类名或接口名为键
private $_reflections = [];
// 用于缓存依赖信息,以类名或接口名为键
private $_dependencies = [];
DI容器的5个数组内容和作用,如图:
DI容器5个数组示意图
注册依赖
使用DI容器,首先要告诉容器,类型及类型之间的依赖关系,声明一这关系的过程称为注册依赖。
使用 yii\di\Container::set()
和 yii\di\Container::setSinglton()
可以注册依赖。
DI容器是怎么管理依赖的呢?要先看看 yii\di\Container::set()
和 yii\di\Container::setSinglton()
public function set($class, $definition = [], array $params = [])
{
// 规范化 $definition 并写入 $_definitions[$class]
$this->_definitions[$class] = $this->normalizeDefinition($class,
$definition);
// 将构造函数参数写入 $_params[$class]
$this->_params[$class] = $params;
// 删除$_singletons[$class]
unset($this->_singletons[$class]);
return $this;
}
public function setSingleton($class, $definition = [], array $params = [])
{
// 规范化 $definition 并写入 $_definitions[$class]
$this->_definitions[$class] = $this->normalizeDefinition($class,
$definition);
// 将构造函数参数写入 $_params[$class]
$this->_params[$class] = $params;
// 将$_singleton[$class]置为null,表示还未实例化
$this->_singletons[$class] = null;
return $this;
}
这两个函数功能类似没有太大区别,只是 set() 用于在每次请求时构造新的实例返回, 而 setSingleton() 只维护一个单例,每次请求时都返回同一对象。
表现在数据结构上,就是 set() 在注册依赖时,会把使用 setSingleton() 注册的依赖删除。 否则,在解析依赖时,你让Yii究竟是依赖续弦还是原配? 因此,在DI容器中,依赖关系的定义是唯一的。 后定义的同名依赖,会覆盖前面定义好的依赖。
从形参来看,这两个函数的 $class 参数接受一个类名、接口名或一个别名,作为依赖的名称。 $definition 表示依赖的定义,可以是一个类名、配置数组或一个PHP callable。
这两个函数,本质上只是将依赖的有关信息写入到容器的相应数组中去。 在 set() 和 setSingleton() 中,
首先调用 yii\di\Container::normalizeDefinition()
对依赖的定义进行规范化处理,其代码如下:
protected function normalizeDefinition($class, $definition)
{
// $definition 是空的转换成 ['class' => $class] 形式
if (empty($definition)) {
return ['class' => $class];
// $definition 是字符串,转换成 ['class' => $definition] 形式
} elseif (is_string($definition)) {
return ['class' => $definition];
// $definition 是PHP callable 或对象,则直接将其作为依赖的定义
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
// $definition 是数组则确保该数组定义了 class 元素
} elseif (is_array($definition)) {
if (!isset($definition['class'])) {
if (strpos($class, '\\') !== false) {
$definition['class'] = $class;
} else {
throw new InvalidConfigException(
"A class definition requires a \"class\" member.");
}
}
return $definition;
// 这也不是,那也不是,那就抛出异常算了
} else {
throw new InvalidConfigException(
"Unsupported definition type for \"$class\": "
. gettype($definition));
}
}
规范化处理的流程如下:
- 如果 $definition 是空的,直接返回数组
['class' => $class]
- 如果 $definition 是字符串,那么认为这个字符串就是所依赖的类名、接口名或别名, 那么直接返回数组
['class' => $definition]
- 如果 $definition 是一个PHP callable,或是一个对象,那么直接返回该 $definition
- 如果 $definition 是一个数组,那么其应当是一个包含了元素
$definition['class']
的配置数组。 如果该数组未定义$definition['class']
那么,将传入的 $class 作为该元素的值,最后返回该数组。 - 上一步中,如果
definition['class']
未定义,而 $class 不是一个有效的类名,那么抛出异常。 - 如果
$definition
不属于上述的各种情况,也抛出异常。
总之,对于 $_definitions
数组中的元素,它要么是一个包含了”class” 元素的数组,要么是一个PHP callable, 再要么就是一个具体对象。
这就是规范化后的最终结果。
在调用 normalizeDefinition()
对依赖的定义进行规范化处理后, set()
和 setSingleton()
以传入的 $class 为键,
将定义保存进 $_definition[]
中, 将传入的 $param 保存进 $_params[]
中。
对于 set()
而言,还要删除 $_singleton[]
中的同名依赖。
对于 setSingleton()
而言,则要将 $_singleton[]
中的同名依赖设为 null , 表示定义了一个Singleton,但是并未实现化。
举几个具体的依赖定义及相应数组的内容变化为例,以加深理解:
$container = new \yii\di\Container;
// 直接以类名注册一个依赖,虽然这么做没什么意义。
// $_definition['yii\db\Connection'] = [‘class’ => 'yii\db\Connetcion']
$container->set('yii\db\Connection');
// 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并供有依赖需要的类使用。
// $_definition['yii\mail\MailInterface'] = ['class' => 'yii\swiftmailer\Mailer']
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
// 注册一个别名,当调用$container->get('foo')时,可以得到一个 yii\db\Connection 实例。
// $_definition['foo'] = ['class' => 'yii\db\Connection']
$container->set('foo', 'yii\db\Connection');
// 用一个配置数组来注册一个类,需要这个类的实例时,这个配置数组会发生作用。
/* $_definition['yii\db\Connection'] = [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
] */
$container->set('yii\db\Connection', [
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
// 用一个配置数组来注册一个别名,由于别名的类型不详,因此配置数组中需要有 class 元素。
/* $_definition['db'] = [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
] */
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
// 用一个PHP callable来注册一个别名,每次引用这个别名时,这个callable都会被调用。
// $_definition['db'] = function(...){...}
$container->set('db', function ($container, $params, $config) {
return new \yii\db\Connection($config);
});
// 用一个对象来注册一个别名,每次引用这个别名时,这个对象都会被引用。
// $_definition['pageCache'] = anInstanceOfFileCache
$container->set('pageCache', new FileCache);
setSingleton()
对于 $_definition
和 $_params
数组产生的影响与 set()
是一样一样的。 不同之处在于,
使用 set()
会 unset $_singltons
中的对应元素,Yii认为既然你都调用 set()
了,说明你希望这个依赖不再是单例了。
而 setSingleton()
相比较于 set()
,会额外地将 $_singletons[$class]
置为 null
。
以此来表示这个依赖已经定义了一个单例,但是尚未实例化。
从 set()
和 setSingleton()
来看, 可能还不容易理解DI容器,比如我们说DI容器中维护了5个数组,但是依赖注册过程只涉及到其中3个。
剩下的 $_reflections
和 $_dependencies
是在解析依赖的过程中完成构建的。
从DI容器的5个数组来看也好,从容器定义了 set()
和 setSingleton()
两个定义依赖的方法来看也好,
不难猜出DI容器中装了两类实例,一种是单例,每次向容器索取单例类型的实例时,得到的都是同一个实例;
另一类是普通实例,每次向容器索要普通类型的实例时,容器会根据依赖信息创建一个新的实例给你。
单例类型主要用于节省构建实例的时间、节省保存实例的内存、共享数据等。而普通类型主要用于避免数据冲突。
对象的实例化
对象的实例化过程要比依赖的定义过程复杂得多。毕竟依赖的定义只是往特定的数据结构 $_singletons
$_definitions
和 $_params
3个数组写入有关的信息。
而对象的实例化过程要相对复杂,这一过程会涉及到复杂依赖关系的解析、涉及依赖单元的实例化等过程。 且让我们抽丝剥茧地进行分析。
解析依赖信息
容器在获取实例之前,必须解析依赖信息。 这一过程会涉及到DI容器中尚未提到的另外2个数组 $_reflections
和 $_dependencies
。
yii\di\Container::getDependencies()
会向这2个数组写入信息,而这个函数又会在创建实例时,由 yii\di\Container::build()
所调用。
如它的名字所示意的, yii\di\Container::getDependencies()
方法用于获取依赖信息,让我们先来看看这个函数的代码:
protected function getDependencies($class)
{
// 如果已经缓存了其依赖信息,直接返回缓存中的依赖信息
if (isset($this->_reflections[$class])) {
return [$this->_reflections[$class], $this->_dependencies[$class]];
}
$dependencies = [];
// 使用PHP5 的反射机制来获取类的有关信息,主要就是为了获取依赖信息
$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();
// 构造函数没有默认值,则为其创建一个引用。 就是前面提到的 Instance 类型。
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
// 将 ReflectionClass 对象缓存起来
$this->_reflections[$class] = $reflection;
// 将依赖信息缓存起来
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
前面讲了 $_reflections
数组用于缓存 ReflectionClass 实例,$_dependencies
数组用于缓存依赖信息。
这个 yii\di\Container::getDependencies()
方法实质上就是通过PHP5 的反射机制, 通过类的构造函数的参数分析他所依赖的单元。
然后统统缓存起来备用。
为什么是通过构造函数来分析其依赖的单元呢? 因为这个DI容器设计出来的目的就是为了实例化对象及该对象所依赖的一切单元。 也就是说,DI容器必然构造类的实例,必然调用构造函数,那么必然为构造函数准备并传入相应的依赖单元。 这也是我们上面讲到的构造函数依赖注入的后续延伸应用。
可能有人会问,那不是还有setter注入么,为什么不用解析setter注入函数的依赖呢? 这是因为要获取实例不一定需要为某属性注入外部依赖单元 。 当然,有时候一个用于注入的属性必须在实例化时指定依赖单元。 这个时候,必然在其构造函数中有一个用于接收外部依赖单元的形式参数。 使用DI容器的目的是自动实例化,只是实例化而已,就意味着只需要调用构造函数。 至于setter注入可以在实例化后操作嘛。
另一个与解析依赖信息相关的方法就是 yii\di\Container::resolveDependencies()
。 它也是关乎 $_reflections
和 $_dependencies
数组的,
它使用 yii\di\Container::getDependencies()
在这两个数组中写入的缓存信息,作进一步具体化的处理。从函数名来看,
他的名字表明是用于解析依赖信息的。 下面我们来看看它的代码:
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
// 前面getDependencies() 函数往 $_dependencies[] 中 写入的是一个 Instance 数组
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
// 向容器索要所依赖的实例,递归调用 yii\di\Container::get()
$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\Container::resolveDependencies()
作用在于处理依赖信息,
将依赖信息中保存的Istance实例所引用的类或接口进行实例化。
综合上面提到的 yii\di\Container::getDependencies()
和 yii\di\Container::resolveDependencies()
两个方法,我们可以了解到:
$_reflections
以类(接口、别名)名为键, 缓存了这个类(接口、别名)的ReflcetionClass。一经缓存,便不会再更改。$_dependencies
以类(接口、别名)名为键,缓存了这个类(接口、别名)的依赖信息。- 这两个缓存数组都是在
yii\di\Container::getDependencies()
中完成。这个函数只是简单地向数组写入数据。 - 经过
yii\di\Container::resolveDependencies()
处理,DI容器会将依赖信息转换成实例。 这个实例化的过程中,是向容器索要实例。也就是说,有可能会引起递归。
实例的创建
解析完依赖信息,就万事俱备了,那么东风也该来了。实例的创建,秘密就在 yii\di\Container::build() 函数中:
protected function build($class, $params, $config)
{
// 调用上面提到的getDependencies来获取并缓存依赖信息,留意这里 list 的用法
list ($reflection, $dependencies) = $this->getDependencies($class);
// 用传入的 $params 的内容补充、覆盖到依赖信息中
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
// 这个语句是两个条件:
// 一是要创建的类是一个 yii\base\Object 类,这个类对于构造函数的参数是有一定要求的。
// 二是依赖信息不为空,也就是要么已经注册过依赖,要么为build() 传入构造函数参数。
if (!empty($dependencies) && is_a($class, 'yii\base\Object', true)) {
// 按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
$dependencies[count($dependencies) - 1] = $config;
// 解析依赖信息,如果有依赖单元需要提前实例化,会在这一步完成
$dependencies = $this->resolveDependencies($dependencies, $reflection);
// 实例化这个对象
return $reflection->newInstanceArgs($dependencies);
} else {
// 会出现异常的情况有二:
// 一是依赖信息为空,也就是你前面又没注册过,现在又不提供构造函数参数,你让Yii怎么实例化?
// 二是要构造的类,根本就不是 Object 类。
$dependencies = $this->resolveDependencies($dependencies, $reflection);
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
从这个 yii\di\Container::build()
来看:
- DI容器只支持
yii\base\Object
类。也就是说,你只能向DI容器索要yii\base\Object
及其子类。 再换句话说,如果你想你的类可以放在DI容器里,那么必须继承自yii\base\Object
类。 但Yii中几乎开发者在开发过程中需要用到的类,都是继承自这个类。 一个例外就是上面提到的yii\di\Instance
类。但这个类是供Yii框架自己使用的,开发者无需操作这个类。 - 递归获取依赖单元的依赖在于
dependencies = $this->resolveDependencies($dependencies, $reflection)
中。 getDependencies()
和resolveDependencies()
为 build() 所用。 也就是说,只有在创建实例的过程中,DI容器才会去解析依赖信息、缓存依赖信息。
特别说明:这个build()方法已经改版了:
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
list ($reflection, $dependencies) = $this->getDependencies($class);
// 用传入的 $params 的内容补充、覆盖到依赖信息中
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
$dependencies = $this->resolveDependencies($dependencies, $reflection);
// 如果$reflection不可实例化,则抛出异常
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
// 如果$config为空,则从给出的$dependencies参数创建一个新的类实例
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}
// $config不为空
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// 按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
// 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);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
}
容器内容实例化的大致过程
与注册依赖时使用 set() 和 setSingleton() 对应,获取依赖实例化对象使用 yii\di\Container::get() ,其代码如下:
public function get($class, $params = [], $config = [])
{
// 已经有一个完成实例化的单例,直接引用这个单例
if (isset($this->_singletons[$class])) {
return $this->_singletons[$class];
// 是个尚未注册过的依赖,说明它不依赖其他单元,或者依赖信息不用定义,则根据传入的参数创建一个实例
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}
// 注意这里创建了 $_definitions[$class] 数组的副本
$definition = $this->_definitions[$class];
// 依赖的定义是个 PHP callable,调用之
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;
}
get() 用于返回一个对象或一个别名所代表的对象。可以是已经注册好依赖的,也可以是没有注册过依赖的。 无论是哪种情况,Yii均会自动解析将要获取的对象对外部的依赖。
get() 接受3个参数:
- $class 表示将要创建或者获取的对象。可以是一个类名、接口名、别名。
- $params 是一个用于这个要创建的对象的构造函数的参数,其参数顺序要与构造函数的定义一致。 通常用于未定义的依赖。
- $config 是一个配置数组,用于配置获取的对象。通常用于未定义的依赖,或覆盖原来依赖中定义好的配置。
get() 解析依赖获取对象是一个自动递归的过程,也就是说,当要获取的对象依赖于其他对象时, Yii会自动获取这些对象及其所依赖的下层对象的实例。 同时,即使对于未定义的依赖,DI容器通过PHP的Reflection API,也可以自动解析出当前对象的依赖来。
get() 不直接实例化对象,也不直接解析依赖信息。而是通过 build() 来实例化对象和解析依赖。
get() 会根据依赖定义,递归调用自身去获取依赖单元。 因此,在整个实例化过程中,一共有两个地方会产生递归:一是 get() , 二是 build() 中的 resolveDependencies() 。
DI容器解析依赖实例化对象过程大体上是这么一个流程:
- 以传入的 $class 看看容器中是否已经有实例化好的单例,如有,直接返回这一单例。
- 如果这个 $class 根本就未定义依赖,则调用 build() 创建之。具体创建过程等下再说。
- 对于已经定义了这个依赖,如果定义为PHP callable,则解析依赖关系,并调用这个PHP callable。 具体依赖关系解析过程等下再说。
- 如果依赖的定义是一个数组,首先取得定义中对于这个依赖的 class 的定义。 然后将定义中定义好的参数数组和配置数组与传入的参数数组和配置数组进行合并, 并判断是否达到终止递归的条件。 从而选择继续递归解析依赖单元,或者直接创建依赖单元。
从 get() 的代码可以看出:
- 对于已经实例化的单例,使用 get() 时只能返回已经实例化好的实例, $params 参数和 $config 参数失去作用。这点要注意, Yii不会提示你,所给出的参数不会发生作用的。 有的时候发现明明已经给定配置数组了,怎么配置不起作用呀?就要考虑是不是因为这个原因了。
- 对于定义为数组的依赖,在合并配置数组和构造函数参数数组过程中, 定义中定义好的两个数组会被传入的 $config 和 $params 的同名元素所覆盖, 这就提供了获取不同实例的可能。
- 在定义依赖时,无论是使用 set() 还是使用 setSingleton() 只要依赖定义为特定对象或特定实例的, Yii均将其视为单例。在获取时,也将返回这一单例。
实例分析
刚开始提到那个例子,下面继续分析。
经过前面的几个 set() 语句之后,DI容器的 $_params 数组是空的, $_singletons 数组也是空的。 $_definintions 数组却有了新的内容:
$_definitions = [
'yii\db\Connection' => [
'class' => 'yii\db\Connection', // 注意这里
'dsn' => ...
],
'app\models\UserFinderInterface' => ['class' => 'app\models\UserFinder'],
'userLister' => ['class' => 'app\models\UserLister'] // 注意这里
];
调用 get('userLister')
过程,见下图:
特别说明:
* 第7步 结果那写错了,应该是 Instance::of('app\models\UserFinderInterface');
* 第8步 结果那写错了,应该是 $_dependencies['app\models\UserLister'] = Instance::of('app\models\UserFinderInterface');
* 第11步 结果那写错了,应该是 get('app\models\UserFinderInterface');
* 第12步 结果那写错了,应该是 $_definitions['app\models\UserFinderInterface'] = ['class' => 'app\models\UserFinder'];
* 第28步 结果那写错了,应该是 $_dependencies['yii\db\Connection'] = [Instance::of(null)];
DI容器解析依赖获取实例的过程示意图
在 DI容器解析依赖获取实例的过程示意图 中绿色方框表示DI容器的5个数组,浅蓝色圆边方框表示调用的函数和方法。 蓝色箭头表示读取内存,红色箭头表示写入内存,虚线箭头表示参照的内存对象,粗线绿色箭头表示回溯过程。 图中3个圆柱体表示实例化过程中,创建出来的3个实例。
对于 get()
函数:
- 在第1步中调用
get('userLister')
表示要获得一个 userLister 实例。 这个 userLister 不是一个有效的类名,说明这是一个别名。 那么要获取的是这个别名所代表的类的实例。 - 查找
$_definitions
数组,发现$_definitions['userLister'] = ['class'=>'app\models\UserLister']
。 这里 userLister 不等于app\models\UserLister
, 说明要获取的这个 userLister 实例依赖于app\models\UserLister
。 这是查找依赖定义数组的第一种情况。 - 而在第22、23步中,
get("yii\db\Connection")
调用get()
时指定要获取的实例的类型, 与依赖定义数组$_definitions
定义的所依赖的类型是相同的,都是yii\db\Connection
。 也就是说,自己依赖于自己,这就基本达到了停止递归调用 get() 的条件,差不多可以开始反溯了。 这是查找依赖定义数组的第二种情况。 - 第三种情况是第3、4步、第13、14步查找依赖定义数组,发现依赖不存在。 说明所要获取的类型的依赖关系未在容器中注册。 对于未注册依赖关系的,DI容器认为要么是一个没有外部依赖的简单类型, 要么是一个容器自身可以自动解析其依赖关系的类型。
- 对于第一种情况,要获取的类型依赖于其他类型的,递归调用
get()
获取所依赖的类型。 - 对于第二、三种情况,直接调用
build()
尝试获取该类型的实例。
build()
在实例化过程中,干了这么几件事:
- 调用
getDependencies()
获取依赖信息。 - 调用
resolveDependencies()
解析依赖信息。 - 将定义中的配置数组、构造函数参数与调用
get()
时传入的配置数组和构造参数进行合并。 这一步并未在上面的示意图中体现,请参阅build()
的源代码部分。 - 根据解析回来的依赖单元,调用
newInstanceArgs()
创建实例。 请留意第36、42步,并非直接由resolveDependencies()
调用newInstanceArgs()
。 而是resolveDependencies()
将依赖单元返回后,由build()
来调用。就像第31步一样。 - 将获取的类型实例返回给调用它的
get()
。
getDependencies()
函数总是被 build()
调用,他干了这么几件事:
- 创建ReflectionClass,并写入
$_reflections
缓存数组。如第6步中,$_reflections['app\models\UserLister'] = new ReflectionClass('app\models\UserLister')
。 - 利用PHP的Reflection API,通过分析构造函数的形式参数,了解到当前类型对于其他单元、默认值的依赖。
- 将上一步了解到的依赖,在
$_dependencies
缓存数组中写入一个 Instance 实例。如第7、8步。 - 当一个类型的构造函数的参数列表中,没有默认值、参数都是简单类型时,得到一个
[null]
。 如第28步。
resolveDependencies()
函数总是被 build()
调用,他在实例化时,干了这么几件事:
- 根据缓存在
$_dependencies
数组中的 Instance 实例的 id , 递归调用容器的get()
实例化依赖单元。并返回给build()
接着运行。 - 对于像第28步之类的依赖信息为
[null]
的,则什么都不干。
newInstanceArgs()
函数是PHP Reflection API的函数,用于创建实例, 具体查看手册
这里只是简单的举例子而已,还没有涉及到多依赖和单例的情形,但是在原理上是一样的。
从上面的例子中不难发现,DI容器维护了两个缓存数组 $_reflections
和 $_dependencies
。这两个数组只写入一次,就可以无限次使用。
因此,减少了对ReflectionClass的使用,提高了DI容器解析依赖和获取实例的效率。
另一方面,我们看到,获取一个实例,步骤其实不少。但是,对于典型的Web应用而言, 有许多模块其实应当注册为单例的,
比如上面的 yii\db\Connection
。 一个Web应用一般使用一个数据库连接,特殊情况下会用多几个,所以这些数据库连接一般是给定不同别名加以区分后,
分别以单例形式放在容器中的。因此,实际获取实例时,步骤会简单得。对于单例, 在第一次 get()
时,直接就返回了。而且,省去不重复构造实例的过程。
这两个方面,都体现出Yii高效能的特点。
上面我们分析了DI容器,这只是其中的原理部分,具体的运用,我们将结合 服务定位器(Service Locator) 来讲。
Reflection API实例
Reflection API 反射的使用实例,看看研究下。
<?php
class A
{
protected $aa = 123;
}
class B
{
private $ba;
public $bb;
protected $bc = 321;
public function __construct(A $a, $b = 1)
{
$this->ba = $a;
$this->bb = $b;
}
}
$reflection = new ReflectionClass('B');
echo "reflection:</br>";
var_dump($reflection);
echo "</br>";
$constructor = $reflection->getConstructor();
echo "constructor:</br>";
var_dump($constructor);
echo "</br>";
echo "constructor->getParameters():</br>";
var_dump($constructor->getParameters());
echo "</br>";
foreach ($constructor->getParameters() as $param)
{
if ($param->isDefaultValueAvailable()) {
echo "param:</br>";
var_dump($param);
echo "</br>";
} else {
$c = $param->getClass();
echo "c:</br>";
var_dump($c);
echo "</br>";
echo "c->getName():</br>";
var_dump($c->getName());
echo "</br>";
}
}
$class = $reflection->newInstanceArgs([new A]);
echo "class:</br>";
var_dump($class);
输出:
reflection:
object(ReflectionClass)#1 (1) {
["name"]=>
string(1) "B"
}
constructor:
object(ReflectionMethod)#2 (2) {
["name"]=>
string(11) "__construct"
["class"]=>
string(1) "B"
}
constructor->getParameters():
array(2) {
[0]=>
object(ReflectionParameter)#3 (1) {
["name"]=>
string(1) "a"
}
[1]=>
object(ReflectionParameter)#4 (1) {
["name"]=>
string(1) "b"
}
}
c:
object(ReflectionClass)#5 (1) {
["name"]=>
string(1) "A"
}
c->getName():
string(1) "A"
param:
object(ReflectionParameter)#3 (1) {
["name"]=>
string(1) "b"
}
class:
object(B)#6 (3) {
["ba":"B":private]=>
object(A)#4 (1) {
["aa":protected]=>
int(123)
}
["bb"]=>
int(1)
["bc":protected]=>
int(321)
}
源码
yii\di\Container
类
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\di;
use ReflectionClass;
use ReflectionException;
use ReflectionNamedType;
use ReflectionParameter;
use Yii;
use yii\base\Component;
use yii\base\InvalidConfigException;
use yii\helpers\ArrayHelper;
/**
* Container implements a [dependency injection](http://en.wikipedia.org/wiki/Dependency_injection) container.
*
* A dependency injection (DI) container is an object that knows how to instantiate and configure objects and
* all their dependent objects. For more information about DI, please refer to
* [Martin Fowler's article](http://martinfowler.com/articles/injection.html).
*
* Container supports constructor injection as well as property injection.
*
* To use Container, you first need to set up the class dependencies by calling [[set()]].
* You then call [[get()]] to create a new class object. The Container will automatically instantiate
* dependent objects, inject them into the object being created, configure, and finally return the newly created object.
*
* By default, [[\Yii::$container]] refers to a Container instance which is used by [[\Yii::createObject()]]
* to create new object instances. You may use this method to replace the `new` operator
* when creating a new object, which gives you the benefit of automatic dependency resolution and default
* property configuration.
*
* Below is an example of using Container:
*
*
* namespace app\models;
*
* use yii\base\BaseObject;
* use yii\db\Connection;
* use yii\di\Container;
*
* interface UserFinderInterface
* {
* function findUser();
* }
*
* class UserFinder extends BaseObject implements UserFinderInterface
* {
* public $db;
*
* public function __construct(Connection $db, $config = [])
* {
* $this->db = $db;
* parent::__construct($config);
* }
*
* public function findUser()
* {
* }
* }
*
* class UserLister extends BaseObject
* {
* public $finder;
*
* public function __construct(UserFinderInterface $finder, $config = [])
* {
* $this->finder = $finder;
* parent::__construct($config);
* }
* }
*
* $container = new Container;
* $container->set('yii\db\Connection', [
* 'dsn' => '...',
* ]);
* $container->set('app\models\UserFinderInterface', [
* 'class' => 'app\models\UserFinder',
* ]);
* $container->set('userLister', 'app\models\UserLister');
*
* $lister = $container->get('userLister');
*
* // which is equivalent to:
*
* $db = new \yii\db\Connection(['dsn' => '...']);
* $finder = new UserFinder($db);
* $lister = new UserLister($finder);
*
*
* For more details and usage information on Container, see the [guide article on di-containers](guide:concept-di-container).
*
* @property-read array $definitions The list of the object definitions or the loaded shared objects (type or
* ID => definition or instance).
* @property-write bool $resolveArrays Whether to attempt to resolve elements in array dependencies.
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Container extends Component
{
/**
* @var array singleton objects indexed by their types
*/
private $_singletons = [];
/**
* @var array object definitions indexed by their types
*/
private $_definitions = [];
/**
* @var array constructor parameters indexed by object types
*/
private $_params = [];
/**
* @var array cached ReflectionClass objects indexed by class/interface names
*/
private $_reflections = [];
/**
* @var array cached dependencies indexed by class/interface names. Each class name
* is associated with a list of constructor parameter types or default values.
*/
private $_dependencies = [];
/**
* @var bool whether to attempt to resolve elements in array dependencies
*/
private $_resolveArrays = false;
/**
* Returns an instance of the requested class.
*
* You may provide constructor parameters (`$params`) and object configurations (`$config`)
* that will be used during the creation of the instance.
*
* If the class implements [[\yii\base\Configurable]], the `$config` parameter will be passed as the last
* parameter to the class constructor; Otherwise, the configuration will be applied *after* the object is
* instantiated.
*
* Note that if the class is declared to be singleton by calling [[setSingleton()]],
* the same instance of the class will be returned each time this method is called.
* In this case, the constructor parameters and object configurations will be used
* only if the class is instantiated the first time.
*
* @param string|Instance $class the class Instance, name, or an alias name (e.g. `foo`) that was previously
* registered via [[set()]] or [[setSingleton()]].
* @param array $params a list of constructor parameter values. Use one of two definitions:
* - Parameters as name-value pairs, for example: `['posts' => PostRepository::class]`.
* - Parameters in the order they appear in the constructor declaration. If you want to skip some parameters,
* you should index the remaining ones with the integers that represent their positions in the constructor
* parameter list.
* Dependencies indexed by name and by position in the same array are not allowed.
* @param array $config a list of name-value pairs that will be used to initialize the object properties.
* @return object an instance of the requested class.
* @throws InvalidConfigException if the class cannot be recognized or correspond to an invalid definition
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
*/
public function get($class, $params = [], $config = [])
{
if ($class instanceof Instance) {
$class = $class->id;
}
if (isset($this->_singletons[$class])) {
// singleton
return $this->_singletons[$class];
} elseif (!isset($this->_definitions[$class])) {
return $this->build($class, $params, $config);
}
$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)) {
// singleton
$this->_singletons[$class] = $object;
}
return $object;
}
/**
* Registers a class definition with this container.
*
* For example,
*
* ```php
* // register a class name as is. This can be skipped.
* $container->set('yii\db\Connection');
*
* // register an interface
* // When a class depends on the interface, the corresponding class
* // will be instantiated as the dependent object
* $container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
*
* // register an alias name. You can use $container->get('foo')
* // to create an instance of Connection
* $container->set('foo', 'yii\db\Connection');
*
* // register a class with configuration. The configuration
* // will be applied when the class is instantiated by get()
* $container->set('yii\db\Connection', [
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
* 'username' => 'root',
* 'password' => '',
* 'charset' => 'utf8',
* ]);
*
* // register an alias name with class configuration
* // In this case, a "class" element is required to specify the class
* $container->set('db', [
* 'class' => 'yii\db\Connection',
* 'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
* 'username' => 'root',
* 'password' => '',
* 'charset' => 'utf8',
* ]);
*
* // register a PHP callable
* // The callable will be executed when $container->get('db') is called
* $container->set('db', function ($container, $params, $config) {
* return new \yii\db\Connection($config);
* });
* ```
*
* If a class definition with the same name already exists, it will be overwritten with the new one.
* You may use [[has()]] to check if a class definition already exists.
*
* @param string $class class name, interface name or alias name
* @param mixed $definition the definition associated with `$class`. It can be one of the following:
*
* - a PHP callable: The callable will be executed when [[get()]] is invoked. The signature of the callable
* should be `function ($container, $params, $config)`, where `$params` stands for the list of constructor
* parameters, `$config` the object configuration, and `$container` the container object. The return value
* of the callable will be returned by [[get()]] as the object instance requested.
* - a configuration array: the array contains name-value pairs that will be used to initialize the property
* values of the newly created object when [[get()]] is called. The `class` element stands for the
* the class of the object to be created. If `class` is not specified, `$class` will be used as the class name.
* - a string: a class name, an interface name or an alias name.
* @param array $params the list of constructor parameters. The parameters will be passed to the class
* constructor when [[get()]] is called.
* @return $this the container itself
*/
public function set($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
unset($this->_singletons[$class]);
return $this;
}
/**
* Registers a class definition with this container and marks the class as a singleton class.
*
* This method is similar to [[set()]] except that classes registered via this method will only have one
* instance. Each time [[get()]] is called, the same instance of the specified class will be returned.
*
* @param string $class class name, interface name or alias name
* @param mixed $definition the definition associated with `$class`. See [[set()]] for more details.
* @param array $params the list of constructor parameters. The parameters will be passed to the class
* constructor when [[get()]] is called.
* @return $this the container itself
* @see set()
*/
public function setSingleton($class, $definition = [], array $params = [])
{
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
$this->_singletons[$class] = null;
return $this;
}
/**
* Returns a value indicating whether the container has the definition of the specified name.
* @param string $class class name, interface name or alias name
* @return bool Whether the container has the definition of the specified name.
* @see set()
*/
public function has($class)
{
return isset($this->_definitions[$class]);
}
/**
* Returns a value indicating whether the given name corresponds to a registered singleton.
* @param string $class class name, interface name or alias name
* @param bool $checkInstance whether to check if the singleton has been instantiated.
* @return bool whether the given name corresponds to a registered singleton. If `$checkInstance` is true,
* the method should return a value indicating whether the singleton has been instantiated.
*/
public function hasSingleton($class, $checkInstance = false)
{
return $checkInstance ? isset($this->_singletons[$class]) : array_key_exists($class, $this->_singletons);
}
/**
* Removes the definition for the specified name.
* @param string $class class name, interface name or alias name
*/
public function clear($class)
{
unset($this->_definitions[$class], $this->_singletons[$class]);
}
/**
* Normalizes the class definition.
* @param string $class class name
* @param string|array|callable $definition the class definition
* @return array the normalized class definition
* @throws InvalidConfigException if the definition is invalid.
*/
protected function normalizeDefinition($class, $definition)
{
if (empty($definition)) {
return ['class' => $class];
} elseif (is_string($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));
}
/**
* Returns the list of the object definitions or the loaded shared objects.
* @return array the list of the object definitions or the loaded shared objects (type or ID => definition or instance).
*/
public function getDefinitions()
{
return $this->_definitions;
}
/**
* Creates an instance of the specified class.
* This method will resolve dependencies of the specified class, instantiate them, and inject
* them into the new instance of the specified class.
* @param string $class the class name
* @param array $params constructor parameters
* @param array $config configurations to be applied to the new instance
* @return object the newly created instance of the specified class
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
*/
protected function build($class, $params, $config)
{
/* @var $reflection ReflectionClass */
list($reflection, $dependencies) = $this->getDependencies($class);
$addDependencies = [];
if (isset($config['__construct()'])) {
$addDependencies = $config['__construct()'];
unset($config['__construct()']);
}
foreach ($params as $index => $param) {
$addDependencies[$index] = $param;
}
$this->validateDependencies($addDependencies);
if ($addDependencies && is_int(key($addDependencies))) {
$dependencies = array_values($dependencies);
$dependencies = $this->mergeDependencies($dependencies, $addDependencies);
} else {
$dependencies = $this->mergeDependencies($dependencies, $addDependencies);
$dependencies = array_values($dependencies);
}
$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)
$dependencies[count($dependencies) - 1] = $config;
return $reflection->newInstanceArgs($dependencies);
}
$object = $reflection->newInstanceArgs($dependencies);
foreach ($config as $name => $value) {
$object->$name = $value;
}
return $object;
}
/**
* @param array $a
* @param array $b
* @return array
*/
private function mergeDependencies($a, $b)
{
foreach ($b as $index => $dependency) {
$a[$index] = $dependency;
}
return $a;
}
/**
* @param array $parameters
* @throws InvalidConfigException
*/
private function validateDependencies($parameters)
{
$hasStringParameter = false;
$hasIntParameter = false;
foreach ($parameters as $index => $parameter) {
if (is_string($index)) {
$hasStringParameter = true;
if ($hasIntParameter) {
break;
}
} else {
$hasIntParameter = true;
if ($hasStringParameter) {
break;
}
}
}
if ($hasIntParameter && $hasStringParameter) {
throw new InvalidConfigException(
'Dependencies indexed by name and by position in the same array are not allowed.'
);
}
}
/**
* Merges the user-specified constructor parameters with the ones registered via [[set()]].
* @param string $class class name, interface name or alias name
* @param array $params the constructor parameters
* @return array the merged parameters
*/
protected function mergeParams($class, $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;
}
/**
* Returns the dependencies of the specified class.
* @param string $class class name, interface name or alias name
* @return array the dependencies of the specified class.
* @throws NotInstantiableException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
*/
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 NotInstantiableException(
$class,
'Failed to instantiate component or class "' . $class . '".',
0,
$e
);
}
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
if (PHP_VERSION_ID >= 50600 && $param->isVariadic()) {
break;
}
if (PHP_VERSION_ID >= 80000) {
$c = $param->getType();
$isClass = false;
if ($c instanceof ReflectionNamedType) {
$isClass = !$c->isBuiltin();
}
} else {
try {
$c = $param->getClass();
} catch (ReflectionException $e) {
if (!$this->isNulledParam($param)) {
$notInstantiableClass = null;
if (PHP_VERSION_ID >= 70000) {
$type = $param->getType();
if ($type instanceof ReflectionNamedType) {
$notInstantiableClass = $type->getName();
}
}
throw new NotInstantiableException(
$notInstantiableClass,
$notInstantiableClass === null ? 'Can not instantiate unknown class.' : null
);
} else {
$c = null;
}
}
$isClass = $c !== null;
}
$className = $isClass ? $c->getName() : null;
if ($className !== null) {
$dependencies[$param->getName()] = Instance::of($className, $this->isNulledParam($param));
} else {
$dependencies[$param->getName()] = $param->isDefaultValueAvailable()
? $param->getDefaultValue()
: null;
}
}
}
$this->_reflections[$class] = $reflection;
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
/**
* @param ReflectionParameter $param
* @return bool
*/
private function isNulledParam($param)
{
return $param->isOptional() || (PHP_VERSION_ID >= 70100 && $param->getType()->allowsNull());
}
/**
* Resolves dependencies by replacing them with the actual object instances.
* @param array $dependencies the dependencies
* @param ReflectionClass $reflection the class reflection associated with the dependencies
* @return array the resolved dependencies
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
*/
protected function resolveDependencies($dependencies, $reflection = null)
{
foreach ($dependencies as $index => $dependency) {
if ($dependency instanceof Instance) {
if ($dependency->id !== null) {
$dependencies[$index] = $dependency->get($this);
} elseif ($reflection !== null) {
$name = $reflection->getConstructor()->getParameters()[$index]->getName();
$class = $reflection->getName();
throw new InvalidConfigException("Missing required parameter \"$name\" when instantiating \"$class\".");
}
} elseif ($this->_resolveArrays && is_array($dependency)) {
$dependencies[$index] = $this->resolveDependencies($dependency, $reflection);
}
}
return $dependencies;
}
/**
* Invoke a callback with resolving dependencies in parameters.
*
* This method allows invoking a callback and let type hinted parameter names to be
* resolved as objects of the Container. It additionally allows calling function using named parameters.
*
* For example, the following callback may be invoked using the Container to resolve the formatter dependency:
*
* ```php
* $formatString = function($string, \yii\i18n\Formatter $formatter) {
* // ...
* }
* Yii::$container->invoke($formatString, ['string' => 'Hello World!']);
* ```
*
* This will pass the string `'Hello World!'` as the first param, and a formatter instance created
* by the DI container as the second param to the callable.
*
* @param callable $callback callable to be invoked.
* @param array $params The array of parameters for the function.
* This can be either a list of parameters, or an associative array representing named function parameters.
* @return mixed the callback return value.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
* @since 2.0.7
*/
public function invoke(callable $callback, $params = [])
{
return call_user_func_array($callback, $this->resolveCallableDependencies($callback, $params));
}
/**
* Resolve dependencies for a function.
*
* This method can be used to implement similar functionality as provided by [[invoke()]] in other
* components.
*
* @param callable $callback callable to be invoked.
* @param array $params The array of parameters for the function, can be either numeric or associative.
* @return array The resolved dependencies.
* @throws InvalidConfigException if a dependency cannot be resolved or if a dependency cannot be fulfilled.
* @throws NotInstantiableException If resolved to an abstract class or an interface (since 2.0.9)
* @since 2.0.7
*/
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 (PHP_VERSION_ID >= 80000) {
$class = $param->getType();
if ($class instanceof \ReflectionUnionType || (PHP_VERSION_ID >= 80100 && $class instanceof \ReflectionIntersectionType)) {
$isClass = false;
foreach ($class->getTypes() as $type) {
if (!$type->isBuiltin()) {
$class = $type;
$isClass = true;
break;
}
}
} else {
$isClass = $class !== null && !$class->isBuiltin();
}
} else {
$class = $param->getClass();
$isClass = $class !== null;
}
if ($isClass) {
$className = $class->getName();
if (PHP_VERSION_ID >= 50600 && $param->isVariadic()) {
$args = array_merge($args, array_values($params));
break;
}
if ($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;
}
/**
* Registers class definitions within this container.
*
* @param array $definitions array of definitions. There are two allowed formats of array.
* The first format:
* - key: class name, interface name or alias name. The key will be passed to the [[set()]] method
* as a first argument `$class`.
* - value: the definition associated with `$class`. Possible values are described in
* [[set()]] documentation for the `$definition` parameter. Will be passed to the [[set()]] method
* as the second argument `$definition`.
*
* Example:
* ```php
* $container->setDefinitions([
* 'yii\web\Request' => 'app\components\Request',
* 'yii\web\Response' => [
* 'class' => 'app\components\Response',
* 'format' => 'json'
* ],
* 'foo\Bar' => function () {
* $qux = new Qux;
* $foo = new Foo($qux);
* return new Bar($foo);
* }
* ]);
* ```
*
* The second format:
* - key: class name, interface name or alias name. The key will be passed to the [[set()]] method
* as a first argument `$class`.
* - value: array of two elements. The first element will be passed the [[set()]] method as the
* second argument `$definition`, the second one — as `$params`.
*
* Example:
* ```php
* $container->setDefinitions([
* 'foo\Bar' => [
* ['class' => 'app\Bar'],
* [Instance::of('baz')]
* ]
* ]);
* ```
*
* @see set() to know more about possible values of definitions
* @since 2.0.11
*/
public function setDefinitions(array $definitions)
{
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);
}
}
/**
* Registers class definitions as singletons within this container by calling [[setSingleton()]].
*
* @param array $singletons array of singleton definitions. See [[setDefinitions()]]
* for allowed formats of array.
*
* @see setDefinitions() for allowed formats of $singletons parameter
* @see setSingleton() to know more about possible values of definitions
* @since 2.0.11
*/
public function setSingletons(array $singletons)
{
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);
}
}
/**
* @param bool $value whether to attempt to resolve elements in array dependencies
* @since 2.0.37
*/
public function setResolveArrays($value)
{
$this->_resolveArrays = (bool) $value;
}
}
yii\di\Instance
类
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\di;
use Exception;
use Yii;
use yii\base\InvalidConfigException;
/**
* Instance represents a reference to a named object in a dependency injection (DI) container or a service locator.
*
* You may use [[get()]] to obtain the actual object referenced by [[id]].
*
* Instance is mainly used in two places:
*
* - When configuring a dependency injection container, you use Instance to reference a class name, interface name
* or alias name. The reference can later be resolved into the actual object by the container.
* - In classes which use service locator to obtain dependent objects.
*
* The following example shows how to configure a DI container with Instance:
*
*
* $container = new \yii\di\Container;
* $container->set('cache', [
* 'class' => 'yii\caching\DbCache',
* 'db' => Instance::of('db')
* ]);
* $container->set('db', [
* 'class' => 'yii\db\Connection',
* 'dsn' => 'sqlite:path/to/file.db',
* ]);
*
*
* And the following example shows how a class retrieves a component from a service locator:
*
*
* class DbCache extends Cache
* {
* public $db = 'db';
*
* public function init()
* {
* parent::init();
* $this->db = Instance::ensure($this->db, 'yii\db\Connection');
* }
* }
*
*
* @author Qiang Xue <qiang.xue@gmail.com>
* @since 2.0
*/
class Instance
{
/**
* @var string the component ID, class name, interface name or alias name
*/
public $id;
/**
* @var bool if null should be returned instead of throwing an exception
*/
public $optional;
/**
* Constructor.
* @param string $id the component ID
* @param bool $optional if null should be returned instead of throwing an exception
*/
protected function __construct($id, $optional = false)
{
$this->id = $id;
$this->optional = $optional;
}
/**
* Creates a new Instance object.
* @param string $id the component ID
* @param bool $optional if null should be returned instead of throwing an exception
* @return Instance the new Instance object.
*/
public static function of($id, $optional = false)
{
return new static($id, $optional);
}
/**
* Resolves the specified reference into the actual object and makes sure it is of the specified type.
*
* The reference may be specified as a string or an Instance object. If the former,
* it will be treated as a component ID, a class/interface name or an alias, depending on the container type.
*
* If you do not specify a container, the method will first try `Yii::$app` followed by `Yii::$container`.
*
* For example,
*
* ```php
* use yii\db\Connection;
*
* // returns Yii::$app->db
* $db = Instance::ensure('db', Connection::class);
* // returns an instance of Connection using the given configuration
* $db = Instance::ensure(['dsn' => 'sqlite:path/to/my.db'], Connection::class);
* ```
*
* @param object|string|array|static $reference an object or a reference to the desired object.
* You may specify a reference in terms of a component ID or an Instance object.
* Starting from version 2.0.2, you may also pass in a configuration array for creating the object.
* If the "class" value is not specified in the configuration array, it will use the value of `$type`.
* @param string $type the class/interface name to be checked. If null, type check will not be performed.
* @param ServiceLocator|Container $container the container. This will be passed to [[get()]].
* @return object the object referenced by the Instance, or `$reference` itself if it is an object.
* @throws InvalidConfigException if the reference is invalid
*/
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']);
$component = $container->get($class, [], $reference);
if ($type === null || $component instanceof $type) {
return $component;
}
throw new InvalidConfigException('Invalid data type: ' . $class . '. ' . $type . ' is expected.');
} 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) {
try {
$component = $reference->get($container);
} catch (\ReflectionException $e) {
throw new InvalidConfigException('Failed to instantiate component or class "' . $reference->id . '".', 0, $e);
}
if ($type === null || $component instanceof $type) {
return $component;
}
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.");
}
/**
* Returns the actual object referenced by this Instance object.
* @param ServiceLocator|Container $container the container used to locate the referenced object.
* If null, the method will first try `Yii::$app` then `Yii::$container`.
* @return object the actual object referenced by this Instance object.
*/
public function get($container = null)
{
try {
if ($container) {
return $container->get($this->id);
}
if (Yii::$app && Yii::$app->has($this->id)) {
return Yii::$app->get($this->id);
}
return Yii::$container->get($this->id);
} catch (Exception $e) {
if ($this->optional) {
return null;
}
throw $e;
}
}
/**
* Restores class state after using `var_export()`.
*
* @param array $state
* @return Instance
* @throws InvalidConfigException when $state property does not contain `id` parameter
* @see https://www.php.net/manual/en/function.var-export.php
* @since 2.0.12
*/
public static function __set_state($state)
{
if (!isset($state['id'])) {
throw new InvalidConfigException('Failed to instantiate class "Instance". Required parameter "id" is missing');
}
return new self($state['id']);
}
}
yii\di\NotInstantiableException
类
<?php
/**
* @link http://www.yiiframework.com/
* @copyright Copyright (c) 2008 Yii Software LLC
* @license http://www.yiiframework.com/license/
*/
namespace yii\di;
use yii\base\InvalidConfigException;
/**
* NotInstantiableException represents an exception caused by incorrect dependency injection container
* configuration or usage.
*
* @author Sam Mousa <sam@mousa.nl>
* @since 2.0.9
*/
class NotInstantiableException extends InvalidConfigException
{
/**
* {@inheritdoc}
*/
public function __construct($class, $message = null, $code = 0, \Exception $previous = null)
{
if ($message === null) {
$message = "Can not instantiate $class.";
}
parent::__construct($message, $code, $previous);
}
/**
* @return string the user-friendly name of this exception
*/
public function getName()
{
return 'Not instantiable';
}
}
参考资料
深入理解Yii2.0 » Yii 模式 » 依赖注入和依赖注入容器 http://www.digpage.com/di.html
ReflectionClass::newInstanceArgs https://php.net/manual/zh/reflectionclass.newinstanceargs.php
getInstance( ) 方法 http://www.php.cn/php/php-getInstance.html
PHP »手册 »语言参考 »类与对象 »后期静态绑定 https://www.php.net/manual/zh/language.oop5.late-static-bindings.php
PHP中new self()和new static()的区别探究 https://www.cnblogs.com/shizqiang/p/6277091.html
ReflectionClass https://www.php.net/manual/zh/class.reflectionclass.php
ReflectionClass::getConstructor https://www.php.net/manual/zh/reflectionclass.getconstructor.php
Yii 2.0 权威指南 关键概念(Key Concepts): 依赖注入容器(Dependency Injection Container) https://www.yiichina.com/doc/guide/2.0/concept-di-container
Inversion of Control Containers and the Dependency Injection pattern https://martinfowler.com/articles/injection.html
依赖注入(DI)在PHP中的实现 https://www.cnblogs.com/aksir/p/6781220.html
Composer自动加载类文件 https://getcomposer.org/doc/04-schema.md#autoload