[Yii2笔记]057依赖注入容器(DI,Dependency Injection Container) [ 技术分享 ]
说明
学习Yii Framework 2(易2框架)的过程是漫长的,也是充满乐趣的,以下是我学习Yii2框架时对官网英文资料(请参见原文网址)的翻译和代码实现,提供了较完整的代码,供你参考。不妥之处,请多多指正!
原文网址:
http://www.yiiframework.com/doc-2.0/guide-concept-di-container.html
本文主题:依赖注入容器(DI,Dependency Injection Container)
依赖注入(DI,Dependency Injection)容器是一个对象,它知道如何去实例化一个对象,配置对象和它依赖的所有对象。
Martin Fowler的文章很好的解释了DI容器的用途,在这里我们仅描述Yii提供的DI容器的用法。
Martin Fowler关于DI容器的文章: http://martinfowler.com/articles/injection.html
1、Dependency Injection
Yii2使用yii\di\Container类提供了DI容器的功能,它支持如下类型的依赖注入: 1、构造器注入(Constructor injection) 2、方法注入(Method injection) 3、Setter和属性注入(Setter and property injection) 4、PHP回调注入(PHP callable injection)
1)构造器注入(Constructor injection)
DI容器(container)支持构造器注入,需要为构造器的参数指定类型。当容器创建一个新对象时,参数类型将告知容器该对象所依赖的是哪个类或接口。容器将尝试通过构造器去获取依赖的类或接口,然后把它们注入到一个新对象。例如:
class Foo{
public function __construct(Bar $bar){
}
}
$foo=$container->get('Foo');
//上句就是以下两句的组合动作:
$bar=new Bar;
$foo=new Foo($bar);
2)方法注入(Method injection)
通常依赖的B类都是被传入到A类构造器,在A类的整个生命周期,B类对象在A类对象中都是有效的。方法注入则只需要依赖于B类的一个方法,把它传递到A类的构造器似乎不太可能,或许还会造成常规使用中的诸多困扰。 A类的方法可以象下例中的doSomething()一样去定义:
class MyClass extends \yii\base\Component{
public function __construct(/*轻量级的依赖*/,$config=[]){
}
public function doSomething($param1,\my\heavy\Dependency $something){
//do something with $something
}
}
你调用这个方法有两种方式: 1、传递\my\heavy\Dependency的一个实例 2、使用yii\di\Container::invoke()
$obj=new MyClass(/*...*/);
Yii::$container->invoke([$obj,'doSomething'],['param1'=>42]);//$something将由DI容器提供
3)Setter和属性注入(Setter and property injection)
使用配置可以实现Setter和属性的注入,当注册一个依赖或创建一个新对象时,你可以提供一个配置,容器可以使用配置实现依赖属性注入到相应的Setter或属性中,例如:
use yii\base\Object;
class Foo extends Object{
public $bar;
private $_qux;
public function getQux(){
return $this->_qux;
}
public function setQux(Qux $qux){
$this->_qux=$qux;
}
}
$container->get('Foo',[],[
'bar'=>$container->get('Bar'),
'qux'=>$container->get('Qux'),
]);
提示:yii\dii\Container::get()方法获取它的第三个参数是一个配置数组,这个数组会被正在创建的对象所使用,如果此类实现了yii\base\Configurable接口(例如:yii\base\Object),配置数组会作为最后一个参数传递给类的构造器,另外,在对象被创建后,这个配置还会被应用。
4)PHP回调注入(PHP callable injection)
在这种情况下,容器会使用一个注册的PHP回调函数创建类的新实例,每当调用yii\di\Container::get()时,对应的回调函数都会被执行。回调函数将解析依赖的对象,并将它们注入到新创建的对象中,例如:
$container->set('Foo',function(){
$foo=new Foo(new Bar);
//...其他初始化操作
return $foo;
});
$foo=$container->get('Foo');
要隐藏创建新对象的复杂逻辑,你可以使用一个静态类,例如:
class FooBuilder{
public static function build(){
$foo=new Foo(new Bar);
//...其他初始化操作
return $foo;
}
}
$container->set('Foo',['app\helper\FooBuilder','build']);
$foo=$container->get('Foo');
这样一来,想要配置Foo类的人无需知道它是怎么创建的。
2、Registering Dependencies(注册依赖)
你可以使用yii\di\Container::set()注册依赖,注册除了需要依赖定义(dependency definition)外,还需要依赖名称(dependency name)。一个依赖名称可以是类名称、接口名称或者是一个别名名称,一个依赖定义可以是一个类名称、一个配置数组或一个PHP回调函数。
public function actionContainer(){
$container = new \yii\di\Container;
//按原样注册类名,这种做法可以忽略
$container->set('yii\db\Connection');
//注册一个接口,当一个类依赖这个接口时,对应的类也会象依赖的对象一样被初始化
$container->set('yii\mail\MailInterface', 'yii\swiftmailer\Mailer');
//注册一个别名,可以使用$container->get('foo')创建Connection的实例
$container->set('foo', 'yii\db\Connection');
//使用配置注册一个类,当使用get()初始化类时,这个配置会被应用。
$container->set('yii\db\Connection', [
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
//使用配置注册一个别名,在这种情况下,"class"必须定义以确定使用的类
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
//注册一个PHP回调函数,当$container->get('db')每次被调用时,这个回调函数都会被执行
$container->set('db', function ($container, $params, $config) {
return new \yii\db\Connection($config);
});
//注册一个组件实例,$container->get('pageCache')会返回同一个实例
$container->set('pageCache', new FileCache);
}
小贴士:如果依赖名称与依赖定义是相同的,完全没必要使用DI容器注册它。
使用set()注册一个依赖会每次都产生一个依赖的实例。你可以使用yii\di\Container::setSingleton(),它在注册一个依赖时只产生一个单一的实例:
$container->setSingleton('yii\db\Connection',[
'dsn' =>'mysql:host=127.0.0.1;dbname=demo',
'username'=>'root',
'password'=>'',
'charset'=>'utf8',
]);
3、Resolving Dependencies(解析依赖)
一旦你注册了依赖,你就可以使用DI容器去创建一个新对象,容器将实例化依赖对象并把它们注入到新创建的对象中,这样就实现了自动解析依赖组件。依赖对象的解决是递归的,也就是说,如果一个依赖对象有其他的依赖,那些依赖都会依次自动解析。
你可以使用yii\di\Container::get()创建一个新对象。这个方法接受的依赖名称可以是一个类名称,或是接口名称,或是别名。使用set()或setSingleton注册时,依赖名称可以有,也可以没有。你可以提供一个类构造器参数列表,并为为创建新的对象提供一个配置,例如:
$db=$container->get('db');
$engine=$container->get('app\components\SearchEngine',[$apiKey,$apiSecret],['type'=>1]);
//上两句等效于:
$engine=new \app\components\SearchEngine($apiKey,$apiSectet,['type'=>1]);
实际上,DI容器做的事情比创建一个新对象要多的多,容器首先会检查类构造器,查找出依赖的类或接口名称,然后用递归方式自动解析所依赖的那些对象。
以下代码展示了一个更复杂的例子,UserLister类依赖于一个要实现UserFinderInterface接口的对象;UserFinder类继承这个接口并依赖于Connection对象,所有的依赖通过类构造器的参数类型来声明。通过属性依赖注册,只需简单调用get('userLister'),DI容器可以自动解析这些依赖并创建一个新的UserLister实例。
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);
4、Practical Usage(实际应用)
当你在应用的入口脚本中包含Yii.php文件时,Yii就创建了一个DI容器。可以使用Yii::$container 获取DI容器。当你调用Yii::createObject()方法时,实际会调用容器的get()方法去创建一个新对象。如前所述,DI容器会自动解析它的依赖对象,并把它们注入到新创建的对象中去,因为在Yii2的很多核心代码中都是使用Yii::createObject()去创建一个新对象,所以你可以修改Yii::$container 来自定义全局对象。
例如:你可能在全局范围内自定义yii\widgets\LinkPager中的分页按钮显示的数量:
\Yii::$container->set('yii\widgets\LinkPager',['maxButtonCount'=>5]);
现在,如果你象如下代码一样,在视图中使用了小部件(widget),maxButtonCount属性将被初始化为5,而不是类中定义的默认值10。
echo \yii\widgets\LinkPager::widget();
你仍然可以通过设置DI容器覆盖此值:
echo \yii\widgets\LinkPager::widget(['maxButtonCount'=>20]);
小贴士:无论是哪种类型的值,它都会被覆盖,所以对此属性数组应小心设置,它们不是数组合并操作。
从DI容器的构造器自动注入获益的另一个例子,假定你的控制器类依赖其他一些对象,如酒店查询服务,你可以通过构造器参数声明所依赖的对象,让DI容器来帮你解析它。
namespace app\controllers;
use yii\web\Controller;
use app\components\BookingInterface;
class HotelController extends Controller
{
protected $bookingService;
public function __construct($id, $module, BookingInterface $bookingService, $config = [])
{
$this->bookingService = $bookingService;
parent::__construct($id, $module, $config);
}
}
如果你从浏览器可以访问这个控制器,你会看到一个错误:BookingInterface不能实例化,这是因为你需要告诉DI容器如何去处理这个依赖:
\Yii::$container->set('app\components\BookingInterface','app\components\BookingService');
现在你可以再访问控制器,app\components\BookingService将被创建并以第3个参数注入到控制器的构造器中。
5、When to Register Dependencies(什么时候注册依赖对象?)
因为只是当新对象创建时才需要依赖对象,所以依赖对象的注册应越早越好,推荐的做法如下: 1、如果你是应用的开发者,你可以在应用的入口脚本或包含进入口脚本的文件中,注册依赖的对象。 2、如果你是一个扩展开发者,你可以在扩展的bootstrapping类中注册依赖的对象。
6、Summary(总结)
依赖注入(dependency injection)和服务定位器(sevice locator)都是流行的设计模式,允许以松耦合、可测试的方式构建软件。我们强烈推荐去读读Martin的文章,可以更好的理解依赖注入和服务定位器。
Martin的文章: http://martinfowler.com/articles/injection.html
Yii在依赖注入容器的顶层实现了服务定位器,当一个服务定位器准备创建一个新对象实例时,它将会把这个调用指向DI容器,DI容器会将依赖的对象自动解析。
//-----------------------------------------------
Dependency Injection实例
//+++++++++++++++++++++++++ //属性和Setter注入实例1 D:\phpwork\advanced\frontend\controllers\PostController.php
namespace frontend\controllers;
use yii\web\Controller;
class PostController extends Controller {
public function actionContainer(){
$container = new \yii\di\Container;
/*
$container->set('db', [
'class' => 'yii\db\Connection',
'dsn' => 'mysql:host=127.0.0.1;dbname=demo',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
]);
$aaa=$container->get('db');
*/
$container->set('mongodb', [
'class' => 'yii\mongodb\Connection',
'dsn' => 'mongodb://dev:123456@127.0.0.1:27017/admin',
]);
$aaa=$container->get('mongodb');
var_dump($aaa);
}
测试结果:
http://localhost:8082/post/container
/*
D:\phpwork\advanced\frontend\controllers\PostController.php:232:
object(yii\mongodb\Connection)[92]
public 'dsn' => string 'mongodb://dev:123456@127.0.0.1:27017/admin' (length=42)
public 'options' =>
array (size=0)
empty
public 'driverOptions' =>
array (size=0)
empty
public 'defaultDatabaseName' => null
public 'mongoClient' => null
private '_databases' =>
array (size=0)
empty
private '_events' (yii\base\Component) =>
array (size=0)
empty
private '_behaviors' (yii\base\Component) => null
*/
//+++++++++++++++++++++++++ //属性和Setter注入实例2 D:\phpwork\advanced\frontend\controllers\PostController.php
namespace frontend\controllers;
use yii\web\Controller;
class PostController extends Controller {
public function actionContainer(){
$container = new \yii\di\Container;
$foo=$container->get('\frontend\models\Foo',[],[
'bar'=>$container->get('\frontend\models\Bar'),
'qux'=>$container->get('\frontend\models\Qux'),
]);
echo $foo->bar->barFun('test');
echo $foo->qux->quxFun('QuxTest');
}
D:\phpwork\advanced\frontend\models\Foo.php
<?php
namespace frontend\models;
use yii\base\Component;
class Foo extends Component{
public $bar;
private $_qux;
public function getQux(){
return $this->_qux;
}
public function setQux(Qux $qux){
$this->_qux=$qux;
}
}
D:\phpwork\advanced\frontend\models\Qux.php
<?php
namespace frontend\models;
use yii\base\Component;
class Qux extends Component{
public $barPara1;
public function quxFun($param1){
$this->barPara1=$param1;
echo "<br>This is Qux::quxFun().".$this->barPara1;
}
}
D:\phpwork\advanced\frontend\models\Bar.php
<?php
namespace frontend\models;
use yii\base\Component;
class Bar extends Component{
public $barPara1;
public function barFun($param1){
$this->barPara1=$param1;
echo "<br>This is Bar::barFun().".$this->barPara1;
}
}
测试结果:
http://localhost:8082/post/container
/*
This is Bar::barFun().test
This is Qux::quxFun().QuxTest
*/
//+++++++++++++++++++++++++ //方法注入实例
方法注入(Method Injection)是Yii2框架提供的强大功能,但这个功能到底该怎么用?可能有些朋友还不太明白,在这里我就用一个简单实例说明一下。
//实例要实现的目标:通过方法注入在Foo类中直接调用Bar类的对象方法
//最直观的效果就是,用下面一行代码:
\Yii::$container->invoke([$foo, 'doSomething'], ['param1' => 42]);
//代替下面两行代码:
$bar=new \frontend\models\Bar;
$foo->doSomething(42,$bar);
//话不多说,上源码,看注释: D:\phpwork\advanced\frontend\controllers\PostController.php
namespace frontend\controllers;
use yii\web\Controller;
class PostController extends Controller {
public function actionContainer(){
//创建一个Foo类的对象实例$foo
$foo=new \frontend\models\Foo;
//使用方法注入在Foo类的对象实例中调用Bar类实例的方法
\Yii::$container->invoke([$foo, 'doSomething'], ['param1' => 42]);
}
D:\phpwork\advanced\frontend\models\Foo.php
<?php
namespace frontend\models;
use yii\base\Component;
class Foo extends Component{
public $fooPara1;
//定义方法注入,指定参数$something的类型为\frontend\models\Bar,即使用时自动创建Bar的实例对象,对象名称为$something。
public function doSomething($param1,\frontend\models\Bar $something){
echo "This is Foo::doSomething()<br>";
//调用Bar类的对象$something的方法barFun(),并传入一个参数$param1
$something->barFun($param1);
}
}
D:\phpwork\advanced\frontend\models\Bar.php
<?php
namespace frontend\models;
use yii\base\Component;
class Bar extends Component{
public $barPara1;
public function barFun($param1){
$this->barPara1=$param1;
//barFun中的具体操作
echo "This is Bar::barFun().".$this->barPara1;
}
}
测试结果:
//运行程序:
http://localhost:8082/post/container
//显示的结果:
/*
This is Foo::doSomething()
This is Bar::barFun().42
*/
总结:通过上例运行实现了方法注入的成功调用,在Foo类对象中成功调用了Bar类对象的方法。
//+++++++++++++++++++++++++ //构造器注入实例
构造器注入(Dependency Injection)是Yii2框架提供的强大功能,但这个功能到底该怎么用?可能有些朋友还不太明白,在这里我就用一个简单实例说明一下。
//实例要实现的目标:通过构造器注入在Foo类中自动实例化Bar类的对象
//最直观的效果就是,用下面一行代码:
$foo=$container->get('foo');
//代替下面两行代码:
$bar=new Bar;
$foo=new Foo($bar);
//话不多说,上源码,看注释: D:\phpwork\advanced\frontend\controllers\PostController.php
namespace frontend\controllers;
use yii\web\Controller;
class PostController extends Controller {
public function actionContainer(){
$container = new \yii\di\Container;
//注册一个依赖(Dependency),可以把它理解为一个名称为"foo"的服务,这个服务使用'frontend\models\Foo'类来创建对象实例
$container->set('foo', 'frontend\models\Foo');
//调用'foo'服务来创建一个$foo对象,仅此一行代码即实现了Foo类的对象创建和其所依赖Bar类的对象自动创建。
$foo=$container->get('foo');
//显示新创建的$foo对象的属性
echo "fooPara1:".$foo->fooPara1;
}
}
D:\phpwork\advanced\frontend\models\Foo.php
<?php
namespace frontend\models;
use yii\base\Component;
class Foo extends Component{
public $fooPara1;
//构造器注入,在构造器中为参数$bar指定其依赖的类是Bar(类全名是:frontend\models\Bar,在同一个命名空间下,所以省略了前段路径)
//使用构造器实例化Bar类的实例对象
public function __construct(Bar $bar,$config=[]){
//将$bar->barPara1赋值给Foo的实例对象
$this->fooPara1=$bar->barPara1;
parent::__construct($config);
}
}
D:\phpwork\advanced\frontend\models\Bar.php
<?php
namespace frontend\models;
use yii\base\Component;
class Bar extends Component{
public $barPara1;
public function init(){
parent::init();
//初始化Bar类的对象,给barPara1赋一个初值"123",仅作本例演示之用。
$this->barPara1="123";
}
}
测试结果:
//运行程序:
http://localhost:8082/post/container
//显示的结果:
/*
fooPara1:123
*/
总结:通过上例运行实现了构造器注入的成功调用,在Foo类中自动实例化了Bar类的对象。
(全文完)
阿江
最后登录:2024-03-03
在线时长:186小时21分
- 粉丝94
- 金钱16816
- 威望160
- 积分20276