阿江 2017-10-12 16:40:28 129次浏览 0条回复 0 0 0

说明

学习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类的对象。

(全文完)

    没有找到数据。
您需要登录后才可以回复。登录 | 立即注册