月下博客

Yii2设置非单例的组件

在Yii中,组件是非常重要的概念。Yii中每种类型的应用程序都在源码中定义了必不可少的核心组件(core components)。可以说,组件是构成完整Yii应用程序的基石。

Yii2中,组件是通过DI(Dependency Injection)和SL(Service Locator)来实现的。关于DI和SL,可查看Yii2权威指南中的章节:http://www.yiiframework.com/doc-2.0/guide-concept-di-container.htmlhttp://www.yiiframework.com/doc-2.0/guide-concept-service-locator.html。英文不太好的同学,可以查看深入理解Yii2.0这本书中的设计模式一章(想深入理解Yii2.0,推荐收藏和阅读该书的所有章节)。有了组件,在Yii2.0的程序中,我们能非常方便的注册和获取依赖对象,实现程序的解耦。

Yii2.0中定义和使用组件非常的方便:在config文件中,如下形式即定义了组件:

return [
    ....,  // other configuration
    'components' => [
        'xxx' => [  // xxx is the id/alias/name of the component
            'class' => 'full qualified class name', // unnecessary for core components,
            'prop1' => 'value1',
            'prop2' => 'value2',
            ...,    // other public properties values,
        ],
        ...,  // other components config
    ],
    ..., // extra settings
];

由于Application是Service Locator的子类,从而在程序中可以方便的引用组件: Yii::$app->xxx(Yii2.0中重载了__set魔术方法,如果为了效率,可以使用Yii::$app->getXxx()来获取组件引用)。

最近在看Spring,想到单例的问题,并与Yii2.0进行了对比。对比发现,Yii2.0中通过配置方式定义组件有如下缺点:

  1. 组件都是单例。在web开发中,很多组件确实只需单例模式就够了,比如request, response对象。然而也有非单例的需求,例如数据库(非web应用中,多数据库连接/连接池就比较好使)。
  2. 无法引用已经定义的组件。这个说法不太准确,已经定义的组件可以作为某个属性传入新的组件中;但如果新组件的构造函数依赖于其他组件,在配置中无法做到。

这两个问题促使我又仔细阅读了Yii中的相关概念。通过指南和和API文档,发现Yii确实是实现了IoC和DI。不过由于Yii2.0是全栈式的框架(开箱即用),平常用的又是基于Service Locator的组件,导致对Yii2.0中的DI了解甚少。下面是本次学习和实验的收获:

  1. Yii2.0中确实实现了DI,并且有DI容器。默认的DI容器可通过Yii::$container引用;
  2. 通过DI容器,可以注册和定义依赖。使用的时候,DI容器会自动解析依赖并返回所需要的实例;
  3. 通过DI容器(而非Service Locator),可以让依赖每次都返回新的实例,也可以指定只返回单例;
  4. Yii2.0的Service Locator返回的对象通过DI容器来解析依赖并生成,组件都是单例。

现在回到博文的主题,也就是文章中提出的两个问题:如何定义每次返回新实例的依赖以及引用DI中的其他依赖?答案在于直接引用DI容器而非通过配置文件定义组件。下面是示例:

// 定义测试类
class Dep {}

Class Foo {
    private $dateTime;
    private $dep;

    public function __construct($ts, $dep) {
        $this->dateTime = date('Y-m-d H:i:s', $ts);
        $this->dep = $dep;
    }

    public function printDate() {
        echo $this->dateTime, PHP_EOL;
    }
}

// 下面是测试
...  // 引入必要的类和配置文件
// 注册依赖
$container = Yii::$container;
$container->set('foo', function ($container, $params, $config) {
    return new /Foo(time(), $container->get('dep'));
});
$container->setSingleton('dep', 'Dep');

// 测试示例是否为新的
$container->get('foo')->printDate();
sleep(2);
$container->get('foo')->printDate();

以下是程序输出:

2016-04-16 16:51:54
2016-04-16 16:51:56

通过示例输出,可以证实DI容器确实按照预期返回了两个新的实例。同时,使用DI容器可以直接定义依赖于其他对象的组件,并且与注册顺序无关。直接使用DI容器的一个不便之处当然是无法通过Yii::$app->xxx这种方式引用,只能是Yii::$container->get('xxx')了。