Aura DI包提供了依赖注入的容器系统,有以下特性:
当与工厂类一起使用时,你可以分开进行对象配置,对象构造和对象使用,为开发者提供更大的灵活性和更好的可测试性。虽然想更充分地介绍依赖注入的性质和好处,但超出了本文档的范围。如需更详细地了解“控制反转”和“依赖注入”,请参考Martin Fowler写的文章http://martinfowler.com/articles/injection.html。
Aura DI包内部存在一个返回新DI实例的instance脚本:
<?php $di = require '/path/to/aura.di/scripts/instance.php';
另外,你也可以把Aura DI的'src/'目录添加到自动加载器中,然后使用下面方法实例化:
<?php use aura\di\Manager; use aura\di\Forge; use aura\di\Config; $di = new Manager(new Forge(new Config));
Manager就是DI容器本身。其支撑对象有:
我们不需要直接操作支撑对象;而是通过Manager的方法来使用它们。
我们将在下面的示例中设置一个数据库连接的服务。数据库连接类定义如下:
<?php namespace example\package; class Database { public function __construct($hostname, $username, $password) { // ... make the database connection } }
我们将分四个步骤来讲解DI容器的用法,从最简单的服务创建到更复杂的应用。每种变体都是正确的用法,也都反映了DI容器的优缺点。
在这种变种中,我们使用new操作符实例化对象,并创建服务。
<?php $di->set('database', new \example\package\Database( 'localhost', 'user', 'passwd' ));
这种方法在设置服务的时候就创建了数据库对象。无论我们将来是否要从容器取回对象,它总是被创建的。
在这种变体中,我们在闭包中创建服务,仍然使用new操作符。
<?php $di->set('database', function() { return new \example\package\Database('localhost', 'user', 'passwd'); });
当我们使用$di->get('database')从容器中取回服务时才会创建数据库对象。在闭包中实例化对象可以实现延迟加载数据库对象;如果我们不调用$di->get('database')方法取回对象,那么该对象永远不会被创建。
在这种变体中,我们将使用$di->newInstance()方法代替new操作符。为了达到延迟加载的目的,同样在闭包中实例化对象。
<?php $di->set('database', function() use ($di) { return $di->newInstance('example\package\Database', array( 'hostname' => 'localhost', 'username' => 'user', 'password' => 'passwd', )); });
newInstance()方法使用Forge对象反射到被实例化类的构造函数上。然后传递关联数组作为类构造器参数。数组中元素的顺序无关紧要,缺失的参数将使用类构造器定义的默认值。
在这种变体中,我们将分开处理Database类的配置过程和延迟实例化过程。
<?php $di->params['example\package\Database'] = array( 'hostname' => 'localhost', 'username' => 'user', 'password' => 'passwd', ); $di->set('database', function() use ($di) { return $di->newInstance('example\package\Database'); });
作为对象创建的一部分,Forge对象检查传递的$di->params参数值。这些值在类实例化时将会和构造器默认参数合并,并把合并后的结果传递给构造器(同样,数组中元素顺序是无关紧要的,键名称和构造器参数名匹配就行)。
到目前为止,我们已经成功地把类配置从类实例化过程分离出来,并且能够从容器中延迟加载服务对象。
在这种变体中,我们在实例化对象的时候覆盖$di->params参数的值。
<?php $di->params['example\package\Database'] = array( 'hostname' => 'localhost', 'username' => 'user', 'password' => 'passwd', ); $di->set('database', function() use ($di) { return $di->newInstance('example\package\Database', array( 'hostname' => 'example.com', )); });
实例化时传递的参数优先级高于配置参数值,而配置参数值的优先级又高于类构造器默认的参数值。
要从容器中取回服务对象,调用$di->get()方法即可。
在下面的示例中,我们将会增加一个抽象类Model以及两个具体类BlogModel和WikiModel。主要想法是:所有的Model类要操作数据库表都需要一个Database连接类。
<?php namespace example\package; abstract class Model { protected $db; public function __construct(Database $db) { $this->db = $db; } } class BlogModel extends Model { // ... } class WikiModel extends Model { // ... }
我们将会为BlogModel和WikiModel创建服务,并把数据库服务注入到以上两上Model中作为服务定义一部分。
<?php // default config for the Database class $di->params['example\package\Database'] = array( 'hostname' => 'localhost', 'username' => 'user', 'password' => 'passwd', ); // a database service $di->set('database', function() use ($di) { return $di->newInstance('example\package\Database'); }); // a blog-model service $di->set('blog_model', function() use ($di) { return $di->newInstance('example\package\BlogModel', array( 'db' => $di->get('database'), )); }); // a wiki-model service $di->set('wiki_model', function() use ($di) { return $di->newInstance('example\package\WikiModel', array( 'db' => $di->get('database'), )); });
但是这里如果使用DI容器提供的配置可继承性,我们可以通过类配置来定义数据库服务注入。
<?php // default params for the Database class $di->params['example\package\Database'] = array( 'hostname' => 'localhost', 'username' => 'user', 'password' => 'passwd', ); // default params for the Model class $di->params['example\package\Model'] = array( 'db' => $di->lazyGet('database'), ); // define the database service $di->set('database', function() use ($di) { return $di->newInstance('example\package\Database'); }); // define the blog_model service $di->set('blog_model', function() use ($di) { return $di->newInstance('example\package\BlogModel'); }); // define the wiki_model service $di->set('wiki_model', function() use ($di) { return $di->newInstance('example\package\WikiModel'); });
我们不再需要在实例化的时候设置'db'的值。取而代之的是,子类BlogModel和WikiModel从父类Model中继承该参数。因此,所有Model子类都能通过构造器参数db获得database服务。(当然,如果有需要可以在实例化的时候覆盖'db'的值。)
注意,这里我们使用了lazyGet()方法。这是一个特殊方法,一般与构造器参数和setter方法一起使用。如果我们调用$di->get()方法,容器将会在那会儿实例化服务。但是,如果使用$di->lazyGet()方法,当且仅当配置好的对象被成功实例化后,服务才会被实例化。想像它是某服务的延迟加载封装器,而该服务又是延迟加载的。
理所当然应该有一个lazyNew()方法,此方法是用于实例化对象,而不是获取服务。lazyNew()之于newInstance()就好比lazyGet()之于get()。使用lazyNew()方法,当配置好的类被创建后,才开始创建对象。
我们无需自己写类去获得配置系统的优点。任何具有构造器参数的类都会被配置系统识别,我们要做的仅仅是使用$di->newInstance()方法实例化类。
上面的应用只是为每个模型对象创建服务,这可能有点乏味。我们可能需要创建其他模型,并且我们不想分开创建他们。另外,我们可能要在另一个对象中创建模型对象,这也是工厂类的用武之地。
下面,我们将定义3个新类:为我们创建模型对象的工厂,使用模型工厂的抽像类PageController和需要博客模型的BlogController类。他们的定义如下:
<?php namespace example\package; use aura\di\Forge; class ModelFactory { protected $forge; public function __construct(Forge $forge) { $this->forge = $forge; } public function newInstance($model_name) { $class = 'example\package\Model' . ucfirst($model_name); return $this->forge->newInstance($class); } } abstract class PageController { protected $model_factory; public function __construct(ModelFactory $model_factory) { $this->model_factory = $model_factory; } } class BlogController extends PageController { public function exec() { $blog_model = $this->model_factory('blog'); // ... get data from the blog model and return it ... } }
现在,我们可以像这样建立DI容器:
<?php // default params for database connections $di->params['example\package\Database'] = array( 'hostname' => 'localhost', 'username' => 'user', 'password' => 'passwd', ); // default params for model objects $di->params['example\package\Model'] = array( 'db' => $di->lazyGet('database'), ); // default params for the model factory $di->params['example\package\ModelFactory'] = array( 'forge' => $di->getForge(), ); // default params for page controllers $di->params['example\package\PageController'] = array( 'model_factory' => $di->lazyGet('model_factory'), ); // the database service $di->set('database', function() use ($di) { return $di->newInstance('example\package\Database'); }); // the model factory service $di->set('model_factory', function() use ($di) { return $di->newInstance('example\package\ModelFactory'); });
当我们实例化BlogController类并运行它时。。。
<?php $blog_controller = $di->newInstance('aura\example\BlogController'); echo $blog_controller->exec();
。。。发生了一系列的事件,共分两步完成所有的依赖注入。第一步是实例化BlogController
PageController继承构造器参数'model_factory'服务ModelFactory参数获取DI容器的Forge对象第二步是在BlogController::exec()方法中调用ModelFactory::newInstance()
BlogController::exec()调用ModelFactory::newInstance()ModelFactory::newInstance()请求Forge对象创建一个新的BlogModel(别忘了,Forge是可以获得构造器参数的。)BlogModel从Model继承参数Model能数获取'database'服务最后,BlogController::exec()方法获得了完全配置好的BlogModel对象,在该方法中不需要再做任何配置工作。
上面我们一直在介绍构造器注入。然而,我们同样可以进行setter注入。
请看下面这个示例。。。
<?php namespace example\package; class Foo { protected $db; public function setDb(Database $db) { $this->db = $db; } }
。。。一般来说,类属性的值应使用sertter方法设置,但是我们也可以用下面的注入方法设置他们:
<?php // after construction, the Forge will call Foo::setDb() // and inject the 'database' service object $di->setter['example\package\Foo']['setDb'] = $di->lazyGet('database'); // create a foo_service; on get('foo_service'), the Forge will create the // Foo object, then call setDb() on it per the setter specification above. $di->set('foo_service', function() use ($di) { return $di->newInstance('example\package\Foo'); });
注意到我们在注入方法中使用了lazyGet()。和构造器参数一样,我们可以通过lazyNew()方法创建一个新的Database对象,而不是共享使用Manager中存在的那个:
<?php // after construction, call Foo::setDb() and inject a service object. // we override the default 'hostname' param for the instantiation. $di->setter['example\package\Foo']['setDb'] = $di->lazyNew('example\package\Database', array( 'hostname' => 'example.com', )); // create a foo_service; on get('foo_service'), the Forge will create the // Foo object, then call setDb() on it per the setter specification above. $di->set('foo_service', function() use ($di) { return $di->newInstance('example\package\Foo'); });
setter的配置也是可继承的。如果你有一个example\package\Foo类,如。。。
<?php namespace example\package; class Bar extends Foo { // ... }
。。。你不需要为子类重新添加一个setter;Forge对象会查找它父类的所有setter,并应用它们。(如果你确要为子类添加一个setter,那会覆盖父类的setter。)
Manager本身是一个依赖注入容器,但是你可能希望创建Manager的子容器来处理服务。当你想要一个对象集合,对象之间互相引用,就好像是注册表或服务定位器的一部分,但是你又想保持依赖注入的优势,这就非常有用了。
添加子容器很简单,两行代码就能搞定:
<?php // create a new Container $sub = $di->newContainer('sub_name');
因为子容器是独立于Manager的,所以你也需要为它单独配置$params和$setters。
<?php // get a previously-created Container $sub = $di->getContainer('sub_name');
你可以使用以下方法操作子容器(其实和Container一样):
这些方法和Manager的一模一样,只是服务将会在子容器中创建而不是Manager。
<?php // create the sub-container $sub = $di->newContainer('sub_name'); // configure params for objects in the sub-container $sub->params['example\pacakage\Database']['host'] = 'alt-db.example.com'; // add an alternative Database connection to the sub-container $sub->set('alt_db', function() use ($sub) { return $sub->newInstance('example\package\Database'); });
如果你想在子容器中添加更多的服务,或修改子容器的配置参数和setter,你可以先从Manager中取回子容器,再做相应的处理:
<?php // get the sub-container $sub = $di->get('sub_name'); // add another service to it $sub->set('other', function() use ($sub) { return $sub->newInstance('example\package\Other'); });
最后,你还可以克隆子容器。这会创建子容器的一个拷贝,包括它的配置和服务定义,但会清除子容器中存储的服务对象。拷贝子容器中的服务对象将会在调用时被创建。这就允许你创建多个DI容器,就像独立注册表或服务定位器一样。
<?php // clone 1 will have a set of service definitions and object instances... // definitions ... $clone1 = $di->cloneContainer('sub_name'); // ... and clone 2 will have the same service definitions, but the object // instances will be different and independent from the ones in clone 1 $clone2 = $di->cloneContainer('sub_name');
如果你想把克隆子容器作为参数传递给另一个对象,可以使用lazyCloneContainer()方法。
<?php $di->params['example\package\NeedsContainer']['container'] = $di->lazyCloneContainer('sub_name');
如果我们使用参数、setter、服务和工厂正确地构建依赖注入,我们只需要直接从DI容器中获取一个对象。所有对象都会在DI容器中由工厂对象和/或Forge对象创建。我们永远都不会在任何创建的对象中直接使用DI容器本身。