阿江 2017-10-12 16:40:28 4030次浏览 1条回复 1 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类的对象。

(全文完)

您需要登录后才可以回复。登录 | 立即注册