1. MVC设计模式
2. 依赖注入和依赖注入容器
为了降低代码耦合程度,提高项目的可维护性,Yii 采用多许多当下最流行又相对成熟的设计模式,包 括了依赖注入(Denpdency Injection, DI)和服务定位器(Service Locator)两种模式。
2.1 相关概念
- 依赖倒置原则(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。
2.2 依赖注入
例:第三方 Web Service 实现特定的功能,比如发送邮 件、推送微博等。假设要实现当访客在博客上发表评论后,向博文的作者发送 Email 的功能。
常见代码:
<?php
interface EmailSenderInterface {
public function send();
}
// 定义Gmail邮件服务
class GmailSender implements EmailSenderInterface {
public function send() {
...
}
}
// 定义评论类
class Comment extends 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 提供的服务了,改用 Yahoo 或自建 的邮件服务了。那么,你不得不修改 Comment::init() 里面对 $_eMailSender 的实例化语句:
$this->_eMailSender = MyEmailSender::getInstance();
有什么办法可以不改变 Comment 的代码,就能扩展成对各种邮件服务都支持么?换句话说,有办法将 Comment 和 GmailSender 解耦么?有办法提高 Comment 的普适性、复用性么?
依赖注入就是为了解决这个问题而生的,当然,DI 也不是唯一解决问题的办法,Service Locator 也是可以实现解耦的。
在 Yii 中使用 DI 解耦,有 2 种注入方式:构造函数注入、属性注入。
2.2.1 构造函数注入
<?
class Comment extend yii\db\ActiveRecord {
private $_eMailSender;
public function __construct($emailSender) {
$this->_eMailSender = $emailSender;
}
}
// 实例化两种不同的邮件服务,当然,他们都实现了 EmailSenderInterface
$sender1 = new GmailSender();
$sender2 = new MyEmailSender();
$comment1 = new Comment($sender1);
$comment1->save();
$comment2 = new Comment($sender2);
$comment2->save();
2.2.2 属性注入
与构造函数注入类似,属性注入通过 setter 或 public 成员变量,将所依赖的单元注入到类内部。具体的属性写入,由外部代码决定:
<?
class Comment extend yii\db\ActiveRecord {
private $_eMailSender;
// 定义了一个 setter()
public function setEmailSender($value) {
$this->_eMailSender = $value;
}
}
$sender1 = new GmailSender();
$comment1 = new Comment;
$comment1->eMailSender = $sender1;
$comment1->save();
2.2.3 DI 容器
Yii 的 DI 容器是 yii\di\Container
,他知道如何对对象及对象的所有依赖,和这些依赖的依赖,进行实例化和配置。
1. DI 容器中的内容
- DI 容器中实例的表示
yii\di\Instance 本质上是 DI 容器中对于某一个类实例的引用,它的代码看起来并不复杂:
<?php
class Instance {
// 仅有的属性,用于保存类名、接口名或者别名
public $id;
// 构造函数,仅将传入的 ID 赋值给 $id 属性
protected function __construct($id) {
}
// 静态方法,创建一个 Instance 实例
public static function of($id) {
}
// 静态方法,用于将引用解析成实际的对象,并确保这个对象的类型
public static function ensure($reference, $type = null, $container = null) {
}
// 获取这个实例所引用的实际对象,事实上它调用的是yii\di\Container::get() 来获取实际对象
public function get($container = null) {
}
public static function __set_state($state) {
}
}
对于 yii\di\Instance ,了解:
- 表示的是容器中的内容,代表的是对于实际对象的引用。
- DI 容器可以通过他获取所引用的实际对象。
- 类仅有的一个属性 id 一般表示的是实例的类型。
2. DI 容器的数据结构
在 DI 容器中,维护了 5 个数组,这是 DI 容器功能实现的基础: ```php <? // 用于保存单例 Singleton 对象,以对象类型为键 private $_singletons = [];
// 用于保存依赖的定义,以对象类型为键 private $_definitions = [];
// 用于保存构造函数的参数,以对象类型为键 private $_params = [];
// 用于缓存 ReflectionClass 对象,以类名或接口名为键 private $_reflections = [];
// 用于缓存依赖信息,以类名或接口名为键 private $_dependencies = [];
<a name="eTl8m"></a>
#### 3. 注册依赖
使用 DI 容器,首先要告诉容器,类型及类型之间的依赖关系,声明这一关系的过程称为注册依赖。使用 `yii\di\Container::set()` 和 `yii\di\Container::setSinglton()` 可以注册依赖。<br />yii\di\Container::set() 和 yii\Container::setSinglton():
```php
<?
public function set($class, $definition = [], array $params = []) {
// 规范化 $_definitions 并写入 $_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 = []) {
$this->_definitions[$class] = $this->normalizeDefinition($class, $definition);
$this->_params[$class] = $params;
// 将 $_singleton[$class] 置为 null,表示还未实例化
$this->_singletons[$class] = null;
return $this;
}
这两个函数功能类似没有太大区别,只是 set() 用于在每次请求时构造新的实例,而 setSingleton() 只维护一个单例,每次请求时都返回同一对象。在 DI 容器中,依赖关系的定义是唯一的。后定义的同名依赖,会覆盖前面定义好的依赖。
从形参来看,这两个函数的 $class 参数接受一个类名、接口名或一个别名,作为依赖的名称。$definition 表示依赖的定义,可以是一个类名、配置数组或一个 PHP callable。
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 是 Instance 对象,转换成 ['class' => $definition->id] 形式
// $definition->id 是 类名、接口名或者别名
} elseif ($definition instanceof Instance) {
return ['class' => $definition->id];
// $definition 是 PHP callable 或对象,则直接将其作为依赖的定义
} elseif (is_callable($definition, true) || is_object($definition)) {
return $definition;
// $definition 是数组则确保该数组定义了 class 元素
} 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));
}
在调用 normalizeDefinition() 对依赖的定义进行规范化处理后,对于 $_definitions 数组中的元素,它要么是一个包含了 “class” 元素的数组,要么是一个 PHP callable 或对象,再要么就是一个具体对象。set() 和 setSingleton() 以传入的 $class 为键,将定义保存进 $_definition[] 中,将传入的 $param 保存进 $_params[] 中。
举例:
<?
$container = new \yii\di\Container;
// 直接以类名注册一个依赖,虽然这么做没什么意义。
// $_definition['yii\db\Connection'] = 'yii\db\Connetcion'
$container->set('yii\db\Connection');
// 注册一个接口,当一个类依赖于该接口时,定义中的类会自动被实例化,并供有依赖需要的类使用。
// $_definition['yii\mail\MailInterface', 'yii\swiftmailer\Mailer']
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
// 注册一个别名,当调用 $container->get('foo') 时,可以得到一个yii\db\Connection 实例。
// $_definition['foo', 'yii\db\Connection']
$container->set('foo', 'yii\db\Connection');
// 用一个配置数组来注册一个类,需要这个类的实例时,这个配置数组会发生作用。
// $_definition['yii\db\Connection'] = [...]
$container->set('yii\db\Connection', [
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
// 用一个配置数组来注册一个别名,由于别名的类型不详,因此配置数组中需要有 class 元素。
// $_definition['db'] = [...]
$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 。以此来表示这个依赖已经定义了一个单例,但是尚未实例化。
剩下的 $_reflections 和 $_dependencies 在解析依赖的过程中完成构建。
DI 容器中装了两类实例,一种是单例,每次向容器索取单例类型的实例时,得到的都是同一个实例;另一类是普通实例,每次向容器索要普通类型的实例时,容器会根据依赖信息创建一个新的实例。单例类型主要用于节省构建实例的时间、节省保存实例的内存、共享数据等。而普通类型主要用于避免数据冲突。
4. 对象的实例化
对象的实例化过程要相对复杂,这一过程会涉及到复杂依赖关系的解析、涉及依赖单元的实例化等过程。
- 解析依赖信息
容器在获取实例之前,必须解析依赖信息。这一过程会涉及到 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 = [];
// 使用反射机制来获取类的有关信息,主要就是为了获取依赖信息
try {
$reflection = new ReflectionClass($class);
} catch (\ReflectionException $e) {
throw new InvalidConfigException(
'Failed to instantiate component or class "' . $class . '".', 0, $e);
}
// 通过类的构建函数 __construct() 的参数来解析这个类依赖于哪些单元
$constructor = $reflection->getConstructor();
if ($constructor !== null) {
foreach ($constructor->getParameters() as $param) {
if (version_compare(PHP_VERSION, '5.6.0', '>=') && $param->isVariadic()) {
break;
// 构造函数如果有默认值,将默认值作为依赖。即然是默认值了,就肯定是简单类型。
} elseif ($param->isDefaultValueAvailable()) {
$dependencies[] = $param->getDefaultValue();
// 构造函数没有默认值,则为其创建一个引用。就是前面提到的 Instance 类型。
} else {
$c = $param->getClass();
$dependencies[] = Instance::of($c === null ? null : $c->getName());
}
}
}
// 将 ReflectionClass 对象缓存起来
$this->_reflections[$class] = $reflection;
// 将依赖信息缓存起来
$this->_dependencies[$class] = $dependencies;
return [$reflection, $dependencies];
}
使用 DI 容器的目的是自动实例化,只是实例化而已,就意味着只需要调用构造函数。至于 setter 注入可以在实例化后操作。
另一个与解析依赖信息相关的方法就是 yii\di\Container::resolveDependencies()
。它使用在 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;
}
resolveDependencies() 作用在于处理依赖信息,将依赖信息中保存的 Istance 实例所引用的类或接口进行实例化。
- $_reflections 以类(接口、别名)名为键,缓存了这个类(接口、别名)的 ReflcetionClass。一经缓存, 便不会再更改。
- $_dependencies 以类(接口、别名)名为键,缓存了这个类(接口、别名)的依赖信息。
- 这两个缓存数组都是在 yii\di\Container::getDependencies() 中完成。这个函数只是简单地向数组写入数据。
- 经过 yii\di\Container::resolveDependencies() 处理,DI 容器会将依赖信息转换成实例。这个实例化的过程中,是向容器索要实例。也就是说,有可能会引起递归。
- 实例的创建
解析完依赖信息,就万事俱备了。实例的创建,秘密就在 yii\di\Container::build()
函数中:
<?
protected function build($class, $params, $config) {
// 调用上面提到的 getDependencies 来获取并缓存依赖信息
list($reflection, $dependencies) = $this->getDependencies($class);
// 如果有新传入构造内容,则优先使用
if (isset($config['__construct()'])) {
foreach ($config['__construct()'] as $index => $param) {
$dependencies[$index] = $param;
}
unset($config['__construct()']);
}
// 如果有新传入参数,则优先使用
foreach ($params as $index => $param) {
$dependencies[$index] = $param;
}
// 解析依赖信息,如果有依赖单元需要提前实例化,会在这一步完成
$dependencies = $this->resolveDependencies($dependencies, $reflection);
if (!$reflection->isInstantiable()) {
throw new NotInstantiableException($reflection->name);
}
if (empty($config)) {
return $reflection->newInstanceArgs($dependencies);
}
$config = $this->resolveDependencies($config);
// 这个语句有两个条件:
// 一是,要创建的类是一个 yii\base\Configurable 类,
// 二是,依赖信息不为空,也就是要么已经注册过依赖,要么为 build() 传入构造函数参数。
if (!empty($dependencies) && $reflection->implementsInterface('yii\base\Configurable')) {
// 按照 Object 类的要求,构造函数的最后一个参数为 $config 数组
$dependencies[count($dependencies) - 1] = $config;
// 实例化这个对象
return $reflection->newInstanceArgs($dependencies);
}
// 依赖信息为空(也就是前面没注册过,现在不提供构造函数参数,Yii 无法实例化) ||
// 要构造的类,根本就不是 Configurable 类
$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 容器才会去解析依赖信息、缓存依赖信息。
- 容器内容实例化的大致过程
获取依赖实例化对象使用 yii\di\Container::get()
,与注册依赖时使用 set() 和 setSingleton() 对应,其 代码如下:
<?
public function get($class, $params = [], $config = []) {
if ($class instanceof Instance) {
$class = $class->id;
}
// 已经有一个完成实例化的单例,直接引用这个单例
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() 解析依赖获取对象是一个自动递归的过程,也就是说,当要获取的对象依赖于其他对象时,Yii 会自动获取这些对象及其所依赖的下层对象的实例。同时,即使对于未定义的依赖,DI 容器通过 PHP 的 Reflection API,也可以自动解析出当前对象的依赖来。
get() 接受 3 个参数:
- $class 表示将要创建或者获取的对象。可以是一个类名、接口名、别名。
- $params 是一个用于这个要创建的对象的构造函数的参数,其参数顺序要与构造函数的定义一致。通常用于未定义的依赖。
- $config 是一个配置数组,用于配置获取的对象。通常用于未定义的依赖,或覆盖原来依赖中定义好的配置。
get() 不直接实例化对象,也不直接解析依赖信息。而是通过 build() 来实例化对象和解析依赖。get() 会根据依赖定义,递归调用自身去获取依赖单元。因此,在整个实例化过程中,一共有两个地方会产生递归:一是 get() ,二是 build() 中的 resolveDependencies() 。
DI 容器解析依赖实例化对象过程大体上是这么一个流程:
- 以传入的 $class 看看容器中是否已经有实例化好的单例,如有,直接返回这一单例。
- 如果这个 $class 根本就未定义依赖,则调用 build() 创建之。具体创建过程等下再说。
- 对于已经定义了这个依赖,如果定义为 PHP callable,则解析依赖关系,并调用这个 PHP callable。具体依赖关系解析过程等下再说。
- 如果依赖的定义是一个数组,首先取得定义中对于这个依赖的 class 的定义。然后将定义中定义好的参数数组和配置数组与传入的参数数组和配置数组进行合并,并判断是否达到终止递归的条件。从而选择继续递归解析依赖单元,或者直接创建依赖单元。
- 对于已经实例化的单例,使用 get() 时只能返回已经实例化好的实例,$params 参数和 $config 参数失去作用。
- 对于定义为数组的依赖,在合并配置数组和构造函数参数数组过程中,定义中定义好的两个数组会被传入的 $config 和 $params 的同名元素所覆盖,这就提供了获取不同实例的可能。
- 在定义依赖时,无论是使用 set() 还是使用 setSingleton() 只要依赖定义为特定对象或特定实例的,Yii 均将其视为单例。在获取时,也将返回这一单例。
5. 实例分析
从依赖关系看,这里的 UserLister 类依赖于接口 UserFinderInterface ,而接口有一个实现就是 UserFinder 类,但这类又依赖于 Connection : ```php <?php 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 通常这么做:
```php
<?
$db = new \yii\db\Connection(['dsn' => '...']);
$finder = new UserFinder($db);
$lister = new UserLister($finder);
就是逆着依赖关系,从最底层的 Connection 开始实例化,接着是 UserFinder 最后是 UserLister 。在写代码的时候,这个前后顺序是不能乱的。而且,需要用到的单元,要一个一个提前准备好。对于自己写的可能还比较清楚,对于其他团队成员写的,还要看他的类究竟是依赖了哪些,并一一实例化。这种情况,如果是个别的、少量的还可以接受,如果有个 10 - 20 个的,那就麻烦了。估计光实例化的代码,就可以写满一屏幕了。
最好的方式是使邮件服 务成为一个单例,这样任何模块在需要邮件服务时,使用的其实是同一个实例。那么改成 DI 容器的话,他是这样的:
<?php
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');
- 首先,各 set() 语句没有前后关系的要求,set() 只是写入特定的数据结构,并未涉及具体依赖关系的解析。所以,前后关系不重要,先定义什么依赖,后定义什么依赖没有关系。
- 其次,面根本没有在 DI 容器中定义 UserFinder 对于 Connection 的依赖。但是 DI 容器通过对 UserFinder 构造函数的分析,能了解到这个类会对 Connection 依赖。这个过程是自动的。
- 最后,上面只有一个 get() 看起来好像根本没有实例化其他如 Connection 单元一样,但事实上,DI 容器已经安排好了一切。在获取 userLister 之前,Connection 和 UserFinder 都会被自动实例化。其中,Connection 是根据依赖定义中的配置数组进行实例化的。
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’) 过程示意图:
绿色方框表示 DI 容器的 5 个数组,浅蓝色圆边方框表示调用的函数和方法。蓝色箭头表示读取内存,红色箭头表示写入内存,虚线箭头表示参照的内存对象,粗线绿色箭头表示回溯过程。图中 3 个圆柱体表示实例化过程中,创建出来的 3 个实例。
3. 服务定位器(Service Locator)
上面我们分析了 DI 容器,这只是其中的原理部分,具体的运用,我们将结合服务定位器(Service Locator) 来讲。这一模式的优点有:
- Service Locator 充当了一个运行时的链接器的角色,可以在运行时动态地修改一个类所要选用的服务, 而不必对类作任何的修改。
- 一个类可以在运行时,有针对性地增减、替换所要用到的服务,从而得到一定程度的优化。
实现服务提供方、服务使用方完全的解耦,便于独立测试和代码跨框架复用。
3.1 Service Locator 的基本功能
在 Yii 中 Service Locator 由 yii\di\ServiceLocator 来实现。从代码组织上,Yii 将 Service Locator 放到与 DI 同一层次来对待,都组织在 yii\di 命名空间下:
<?php
class ServiceLocator extends Component {
// 用于缓存服务、组件等的实例
private $_components = [];
// 用于保存服务和组件的定义,通常为配置数组,可以用来创建具体的实例
private $_definitions = [];
// 重载了 getter 方法,使得访问服务和组件就跟访问类的属性一样。
// 同时,也保留了原来 Component 的 getter 所具有的功能。
// ServiceLocator 并未重载 __set(),仍然使用 yii\base\Component::__set()
public function __get($name) {
}
// 对比 Component,增加了对是否具有某个服务和组件的判断。
public function __isset($name) {
if ($this->has($name)) {
return true;
}
return parent::__isset($name);
}
// 当 $checkInstance === false 时,用于判断是否已经定义了某个服务或组件
// 当 $checkInstance === true 时,用于判断是否已经有了某人服务或组件的实例
public function has($id, $checkInstance = false) {
return $checkInstance ? isset($this->_components[$id]) : isset($this->_definitions[$id]);
}
// 根据 $id 获取对应的服务或组件的实例
public function get($id, $throwException = true) {
}
// 用于注册一个组件或服务,其中 $id 用于标识服务或组件。
// $definition 可以是一个类名,一个配置数组,一个 PHP callable,或者一个对象
public function set($id, $definition) {
}
// 删除一个服务或组件
public function clear($id) {
unset($this->_definitions[$id], $this->_components[$id]);
}
// 用于返回 Service Locator 的 $_components 数组或 $_definitions 数组,同时也是 components 属性的 getter 函数
public function getComponents($returnDefinitions = true) {
return $returnDefinitions ? $this->_definitions : $this->_components;
}
// 批量方式注册服务或组件,同时也是 components 属性的 setter 函数
public function setComponents($components) {
foreach ($components as $id => $component) {
$this->set($id, $component);
}
}
}
3.1.1 Service Locator 的数据结构
Service Locator 维护了两个数组,
$_components
和$_definitions
。这两个数组均是以服务或组件的 ID 为键的数组。$_components 用于缓存存在 Service Locator 中的组件或服务的实例。Service Locator 为其提供了 getter 和 setter。使其成为一个可读写的属性。$_definitions 用于保存这些组件或服务的定义。这个定义可以是:配置数组。在向 Service Locator 索要服务或组件时,这个数组会被用于创建服务或组件的实例。与 DI 容器的要求类似,当定义是配置数组时,要求配置数组必须要有 class 元素,表示要创建的是什么类。
- PHP callable。每当向 Service Locator 索要实例时,这个 PHP callable 都会被调用,其返回值,就是所要的对象。对于这个 PHP callable 有一定的形式要求,一是它要返回一个服务或组件的实例。二是它不接受任何的参数。至于具体原因,后面会讲到。
- 对象。这个更直接,每当索要某个特定实例时,直接把这个对象返回。
- 类名。即,使得 is_callable($definition, true) 为真的定义。
yii\di\ServiceLocator::set() 的代码:
<?
public function set($id, $definition) {
// 确保服务或组件 ID 的唯一性
unset($this->_components[$id]);
// 当定义为 null 时,表示要从 Service Locator 中删除一个服务或组件
if ($definition === null) {
unset($this->_definitions[$id]);
return;
}
// 定义如果是个对象或 PHP callable,或类名,直接作为定义保存
if (is_object($definition) || is_callable($definition, true)) {
// 定义的过程,只是写入了 $_definitions 数组
$this->_definitions[$id] = $definition;
// 定义如果是个数组,要确保数组中具有 class 元素
} elseif (is_array($definition)) {
if (isset($definition['__class'])) {
// 定义的过程,只是写入了 $_definitions 数组
$this->_definitions[$id] = $definition;
$this->_definitions[$id]['class'] = $definition['__class'];
unset($this->_definitions[$id]['__class']);
} elseif (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));
}
}
服务或组件的 ID 在 Service Locator 中是唯一的,用于区别彼此。在任何情况下,Service Locator 中同一 ID 只有一个实例、一个定义。也就是说,Service Locator 中,所有的服务和组件,只保存一个单例。这是正常的逻辑,既然称为服务定位器,你只要给定一个 ID,它必然返回一个确定的实例。这一点跟 DI 容器是一样的。(Service Locator 中 ID 仅起标识作用,可以是任意字符串)
批量注册的 yii\di\ServiceLocator::setCompoents() 只不过是简单地遍历数组,循环调用 set() 而已。向 Service Locator 注册服务或组件,其实就是向 $_definitions 数组写入信息。
3.1.2 访问 Service Locator 中的服务
Service Locator 重载了 __get() 使得可以像访问类的属性一样访问已经实例化好的服务和组件:
<?
public function __get($name) {
// has() 方法就是判断 $_definitions 数组中是否已经保存了服务或组件的定义
// 留意,这个时候服务或组件仅是完成定义,不一定已经实例化
if ($this->has($name)) {
// get() 方法用于返回服务或组件的实例
return $this->get($name);
}
// 未定义的服务或组件,那么视为正常的属性、行为,调用父类__get()
return parent::__get($name);
}
例如:
<?
// 创建一个 Service Locator
$serviceLocator = new yii\di\ServiceLocator;
// 注册一个 cache 服务
$serviceLocator->set('cache', [
'class' => 'yii\cache\MemCache',
'servers' => [...],
]);
// 等效于:$serviceLocator->get('cache')->flushValues();
$serviceLocator->cache->fulshValue();
在 Service Locator 中,未重载 __set() 。所以没有 setter 可以使用,需要调用 yii\di\ServiceLocator::set() 对服务和组件进行注册。
3.2 通过 Service Locator 获取实例
Service Locator 通过 yii\di\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];
// 如果定义是个对象,且不是 Closure 对象,那么直接将这个对象返回
if (is_object($definition) && !$definition instanceof Closure) {
// 保存进 $_components 数组中,以后就可以直接引用了
return $this->_components[$id] = $definition;
}
// 是个数组或者 PHP callable,调用 Yii::createObject() 来创建一个实例
// 实例化后,保存进 $_components 数组中,以后就可以直接引用了
return $this->_components[$id] = Yii::createObject($definition);
} elseif ($throwException) {
throw new InvalidConfigException("Unknown component ID: $id");
}
return null;
}
3.3 在 Yii 应用中使用 Service Locator 和 DI 容器
Yii 中是把 Service Locator 和 DI 容器结合起来用的,Service Locator 是建立在 DI 容器之上的。那么一个 Yii 应用,是如何使用 Service Locator 和 DI 容器的呢?
3.3.1 DI 容器的引入
每个 Yii 应用都有一个入口脚本 index.php 。在其中有一行:
<?
require(__DIR__ . '/../../vendor/yiisoft/yii2/Yii.php');
<?php
require __DIR__ . '/BaseYii.php';
class Yii extends \yii\BaseYii {
}
spl_autoload_register(['Yii', 'autoload'], true, true);
Yii::$classMap = require __DIR__ . '/classes.php';
// 创建一个 DI 容器,并由 Yii::$container 引用
Yii::$container = new yii\di\Container();
Yii 是一个工具类,继承自 yii\BaseYii 。但这里对父类的代码没有任何重载,但是,Yii 提供了修改默认功能的机会。就是自己写一个 Yii 类,来扩展、重载 Yii 默认的、由 yii\BaseYii 提供的特性和功能。
可以随时使用 Yii::$container
来访问 DI 容器。一般情况下,如无必须的理由,不要自己创建 DI 容器,使用 Yii::$container 完全足够。
3.3.2 Application 的本质
入口脚本 index.php 的最后一行:
<?
(new yii\web\Application($config))->run();
所有的 Application 都是 Service Locator:
<?
// 首先,yii\web\Application 继承自 yii\base\Application 。
class Application extends \yii\base\Application {}
// 而 yii\base\Application 又继承自 yii\base\Module 。
abstract class Application extends Module {}
// yii\base\Module 继承自 yii\di\ServiceLocator 。
class Module extends ServiceLocator {}
在 Application 的构造函数第一行把自身实例复制给Yii::$app了,这意味着 Yii 应用创建之后,可以随时 通过 Yii::$app 来访问应用自身,也就是访问 Service Locator:
<?
public function __construct($config = []) {
Yii::$app = $this;
}
至此,DI 容器有了,Service Locator 也出现了。
3.3.3 实例创建方法
Service Locator 和 DI 容器的亲密关系就隐藏在 yii\di\ServiceLocator::get()
获取实例时,调用的 Yii::createObject() 中。前面我们说到这个 Yii 继承自 yii\BaseYii ,实际上是 BaseYii::createObject()
,其代码如下:
<?
// static::$container 就是上面说的引用了 DI 容器的静态变量
public static function createObject($type, array $params = []) {
// 字符串,代表一个类名、接口名、别名。
if (is_string($type)) {
return static::$container->get($type, $params);
}
// 是个 PHP callable 则调用其返回一个具体实例(是否有其他依赖)。
if (is_callable($type, true)) {
return static::$container->invoke($type, $params);
}
if (!is_array($type)) {
throw new InvalidConfigException('Unsupported configuration type: ' . gettype($type));
}
// 是个数组,代表配置数组,必须含有 __class 元素。
if (isset($type['__class'])) {
$class = $type['__class'];
unset($type['__class'], $type['class']);
// 调用 DI 容器的 get() 来获取、创建实例
return static::$container->get($class, $params, $type);
}
if (isset($type['class'])) {
$class = $type['class'];
unset($type['class']);
return static::$container->get($class, $params, $type);
}
throw new InvalidConfigException(
'Object configuration must be an array containing a "class" or "__class" element.');
}
Yii中所有的实例(除了 Application,DI 容器自身等入口脚本中实例化的),都是通过 DI 容器来获取的。
同时,不难发现,Yii 的基类 yii\BaseYii ,所有的成员变量和方法都是静态的,其中的 DI 容器是个静态成员变量 $container 。DI 容器就形成了最常见形式的单例模式,在内存中仅有一份,所有的 Service Locator (Module 和 Application)都共用这个 DI 容器。这就节省了大量的内存空间和反复构造实例的时间。
DI 容器的单例化,使得 Yii 不同的模块共用组件成为可能。
当 Service Locator 中服务或组件的定义是一个 PHP callable 时,对其形式有一定要求。一是返回一个实例,二是不接收任何参数。(在 Yii::createObject() 中也可以看出来)
由于 Yii::createObject() 为 yii\di\ServiceLocator::get() 所调用时没有提供第二参数($params)。因此,使用其实例化时,是接收不到任何参数的。
3.4 Yii创建实例的全过程
在向 DI 容器索要一个没有注册过依赖的类型时,DI 容器视为这个类型不依赖于任何类型可以直接创建,或者这个类型的依赖信息容器本身可以通过 Reflection API 自动解析出来,不用提前注册。
在某个配置文件中写了有关的内容的注册服务,如”common/config/main.php”的:
<?
return[
'components' => array(
'cache' => require(__DIR__ . '/components/cache.php'),
'log' => require(__DIR__ . '/components/log.php'),
'db' => [
'class' => 'yii\db\Connection',
],
),
];
这个数组会被 Yii::configure($config) 所调用,然后会变成调用 Application 的 setComponents(),而 Application 其实就是一个 Service Locator。setComponents() 方法又会遍历传入的配 置数组,然后使用使用 Service Locator 的 set() 方法注册服务。
3.5 总结
DI 容器、Service Locator 是如何配合使用的:
- Yii 类提供了一个静态的 $container 成员变量用于引用 DI 容器。在入口脚本中,会创建一个 DI 容器, 并赋值给这个 $container 。
- Service Locator 通过 Yii::createObject() 来获取实例,而这个 Yii::createObject() 是调用了 DI 容器的 yii\di\Container::get() 来向 Yii::$container 索要实例的。因此,Service Locator 最终是通过 DI 容器来创建、获取实例的。
- 所有的 Module,包括 Application 都继承自 yii\di\ServiceLocator ,都是 Service Locator。因此,DI 容器和 Service Locator 就构成了整个 Yii 的基础。