[翻译] Auryn 使用指南
Auryn 是一款递归依赖注入器。使用 Auryn 引导和连接 S.O.L.I.D 和面向对象的 PHP 应用程序。
如何工作
此外,Auryn 基于在类的构造函数签名中指定参数类型提示递归实例化类的依赖关系。这需要使用反射(Reflection)。你可能听说过“反射很慢”。让我们来澄清一些事情:如果你做错了,任何东西都可能“慢”。反射比访问磁盘快一个数量级,比从远程数据库检索信息(比如说)快几个数量级。另外,如果你担心速度的话,每个反射都提供了条件来缓存结果。Auryn 会缓存任何它生成的反射,来最大限度的减少潜在的性能影响。
Auryn 不是服务定位器(Service Locator)。不要通过向你的应用类中传入注入器将其变成它。服务定位器是一种反面模式(Anti-pattern);它隐藏了类依赖项,使得代码难以维护,并且使得你的 API 具有欺骗性!在引导阶段,你应该只使用一个 注入器将应用程序的不同组成部分连接在一起 。
指南
必备条件和安装
- Auryn 需要 PHP 5.3 或更高版本。
安装
Github
你可以在任何时候从 Github 库克隆最新版本的 Auryn。
$ git clone git://github.com/rdlowrey/auryn.git
Composer
你还可以使用 Composer 将 Auryn 作为依赖包含在你项目的 composer.json
文件中。相关的包是 rdlowrey/auryn
。
也可以使用 Composer 命令行工具获取包:
composer require rdlowrey/auryn
手动下载
也可以在 Tags 页面手动下载归档的标记发布版本。
基本用法
要开始使用注入器,只需简单地创建一个新的 Auryn\Injector
(以下简称 Injector)实例类:
<?php
$injector = new Auryn\Injector;
简单实例化
如果一个类没有在它的构造函数签名中指定任何依赖项,则使用 Injector 生成它没有任何意义。然而,为了完整起见,你可以执行以下操作以获得相同的结果:
<?php
$injector = new Auryn\Injector;
$obj1 = new SomeNamespace\MyClass;
$obj2 = $injector->make('SomeNamespace\MyClass');
var_dump($obj2 instanceof SomeNamespace\MyClass); // true
具体的类型提示依赖项
如果一个类只请求具体的依赖项,你可以使用 Injector 注入它们,而不指定任何注入定义。比如,在以下场景中,你可以使用 Injector 自动为 MyClass
提供所需 SomeDependency
和 AnotherDenpendency
类的实例。
<?php
class SomeDependency {}
class AnotherDependency {}
class MyClass {
public $dep1;
public $dep2;
public function __construct(SomeDependency $dep1, AnotherDependency $dep2) {
$this->dep1 = $dep1;
$this->dep2 = $dep2;
}
}
$injector = new Auryn\Injector;
$myObj = $injector->make('MyClass');
var_dump($myObj->dep1 instanceof SomeDependency); // true
var_dump($myObj->dep2 instanceof AnotherDependency); // true
递归依赖实例化
Injector 的关键属性之一是以递归的方式遍历类的依赖关系树来实例化对象。这确实是一种奇怪的说法,“如果你实例化一个请求对象 B 的对象 A,Injector 会实例化任何对象 B 的依赖项,以便 B 会被实例化并提供给 A”。可能通过一个简单的例子可能最好的理解这一点。考虑如下这些类,其中 Car
请求 Engine
,并且 Engine
类含有他自己的具体依赖项:
<?php
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
class Engine {
private $sparkPlug;
private $piston;
public function __construct(SparkPlug $sparkPlug, Piston $piston) {
$this->sparkPlug = $sparkPlug;
$this->piston = $piston;
}
}
$injector = new Auryn\Injector;
$car = $injector->make('Car');
var_dump($car instanceof Car); // true
注入定义
你可能已经注意到,之前的那些例子都展示了类的实例化,这些类带有显式的、类型提示的、具体的构造函数参数。显然,你的很多类不适合这种模式。有些类会类型提示接口和抽象类。有些会指定标量参数(Scalar,PHP 有四种标量类型),这些参数在 PHP 中不提供类型提示。还有其它一些可能是数组的参数等。在这些情况下,我们需要协助 Injector,告诉它我们希望注入的确切内容。
定义构造函数参数的类名
让我们看一下如何为一个类在其构造函数签名中提供一个非具体的类型提示。考虑如下代码,Car
需要 Engine
,而 Engine
是一个接口。
<?php
interface Engine {}
class V8 implements Engine {}
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
在这种情况下为了实例化 Car
,我们只需要提前为类定义一个注入定义。
<?php
$injector = new Auryn\Injector;
$injector->define('Car', ['engine' => 'V8']);
$car = $injector->make('Car');
var_dump($car instanceof Car); // true
需要注意的重要几点如下:
- 自定义的定义是一个
array
,其键名与构造函数的参数名相匹配。 - 定义数组中的值表示为指定的参数键进行注入的类名。
因为我们所需要定义的 Car
构造函数参数名为 $engine
,我们的定义指定了一个名为键名 engine
,其值是我们想要注入的类名(V8
)。
自定义的注入定义是否是必须的,取决于每个参数。比如,在下面这个类中,由于 $arg1
指定了一个具体类的类型提示,所以我们只需要为 $arg2
定义可注入的类。
<?php
class MyClass {
private $arg1;
private $arg2;
public function __construct(SomeConcreteClass $arg1, SomeInterface $arg2) {
$this->arg1 = $arg1;
$this->arg2 = $arg2;
}
}
$injector = new Auryn\Injector;
$injector->define('MyClass', ['arg2' => 'SomeImplementationClass']);
$myObj = $injector->make('MyClass');
注意: 在类型提示的抽象类中注入实例,与上面例子为接口类型提示注入实例的方式完全相同。
在注入定义中使用已存在的实例
注入定义还可以指定一个必需类的预先存在的实例,而不是字符串类名:
<?php
interface SomeInterface {}
class SomeImplementation implements SomeInterface {}
class MyClass {
private $dependency;
public function __construct(SomeInterface $dependency) {
$this->dependency = $dependency;
}
}
$injector = new Auryn\Injector;
$dependencyInstance = new SomeImplementation;
$injector->define('MyClass', [':dependency' => $dependencyInstance]);
$myObj = $injector->make('MyClass');
var_dump($myObj instanceof MyClass); // true
注意: 由于这个
difine()
调用正在传入原始值(通过冒号:
的使用作为依据),因此你能够通过省略数组键并依赖参数顺序(而非名称)得到同样的结果。像是这样:$injector->define('MyClass', [$dependencyInstance]);
。
动态指定注入定义
你也可以在实时调用时用 Auryn\Injector::make
指定注入定义,考虑以下情况:
<?php
interface SomeInterface {}
class SomeImplementationClass implements SomeInterface {}
class MyClass {
private $dependency;
public function __construct(SomeInterface $dependency) {
$this->dependency = $dependency;
}
}
$injector = new Auryn\Injector;
$myObj = $injector->make('MyClass', ['dependency' => 'SomeImplementationClass']);
var_dump($myObj instanceof MyClass); // true
以上代码展示了即便我们没有调用 Injector 的 define
方法,实时调用的规范也允许我们实例化 MyClass
。
注意: 动态实例化定义会覆盖为指定类预先赋予的定义,但是仅限于对
Auryn\Injector::make
的特定调用的上下文中。
类型提示别名
在面向对象设计(Object-oriented desian, OOD)中针对接口编程是非常有用的概念之一。设计良好的代码应该尽可能类型提示接口。但是这是否意味着我们必须在应用中为每一个类分配注入定义,以获取抽象依赖项的好处?好在这个问题的答案是“否”。Injector 通过接受“别名”来达成目标。考虑如下代码:
<?php
interface Engine {}
class V8 implements Engine {}
class Car {
private $engine;
public function __construct(Engine $engine) {
$this->engine = $engine;
}
}
$injector = new Auryn\Injector;
// 告诉 Injector 类随时注入 V8 的实例
// 它遇到 Engine 类型提示
$injector->alias('Engine', 'V8');
$car = $injector->make('Car');
var_dump($car instanceof Car); // bool(true)
在这个例子中,我们演示了如何为任一存在的特定接口或抽象类的类型提示指定一个别名类。一旦分配了实现,Injector 就会用它来为任意参数提供匹配的类型提示。
重要: 如果为实现分配所涵盖的参数定义了注入定义,则该定义优先于实现。
非类名参数
之前的所有示例都用来演示 Injector 类是如何实例化基于类型提示的参数、类名定义以及已存在的实例。但是如果我们希望把标量或其它非对象变量注入到类中会发生什么?首先,让我们创建如下行为规则:
重要: 默认情况下 Injector 假定所有的命名参数定义都是类名。
如果你希望 Injector 以“原始”值而不是类名的方式处理命名参数定义,你必须在你的定义中给参数名添加冒号 :
作为前缀。比如,考虑如下代码,我们告诉 Injector 共享了一个 PDO
数据库连接实例,并且定义它的标量构造函数参数:
<?php
$injector = new Auryn\Injector;
$injector->share('PDO');
$injector->define('PDO', [
':dsn' => 'mysql:dbname=testdb;host=127.0.0.1',
':username' => 'dbuser',
':passwd' => 'dbpass'
]);
$db = $injector->make('PDO');
冒号在参数名前面告诉 Injector 关联的值不是类名。如果上面的代码漏掉了这个冒号,Auryn 将会尝试实例化字符串中指代名称的类,并出现意外结果。此外注意,我们可以轻松在以上定义中指定数组、整数或任何其它数据类型。只要参数名以 :
为前缀,Auryn 将会直接注入该值而不是尝试实例化它。
注意: 正如之前所提到的,由于这个
define()
调用传入的是原始值,你可以选择按照参数顺序而不是名称来分配值。由于 PDO 头三个参数是$dsn
、$username
和$password
,按照这个顺序,你可以不通过数组键实现同样的结果,像这样:$injector->define('PDO', ['mysql:dbname=testdb;host=127.0.0.1', 'dbuser', 'dbpass']);
全局参数定义
有时候应用程序可能会在任何地方重用相同的值。然而,在应用中手动在所有可能用到的地方指定定义是个麻烦事。Auryn 提供 Injector::defineParam()
方法缓解这个问题。考虑如下示例……
<?php
$myUniversalValue = 42;
class MyClass {
public $myValue;
public function __construct($myValue) {
$this->myValue = $myValue;
}
}
$injector = new Auryn\Injector;
$injector->defineParam('myValue', $myUniversalValue);
$obj = $injector->make('MyClass');
var_dump($obj->myValue === 42); // bool(true)
因为我们给 myValue
指定了一个全局定义,所有没被其它方式定义的参数(如下所示),并且与指定参数名相匹配,就会被这个全局值自动填充。如果参数符合如下任何标准,则全局值将不会被使用:
- 类型提示
- 预定义的注入定义
- 自定义的调用时定义
进阶用法
实例共享
在现代 OOP 中普遍存在的困扰之一是单例(Singleton)反面模式。程序员希望将类限制为单个实例,经常会陷入使用 static
单例实现(如配置类和数据库连接)的陷阱。虽然经常必须防止类的多个实例,但是单例方法会有损可测试性,因此一般应该避免。Auryn\Injector
使得在上下文中共享类实例成为小事一桩,同时可以最大化可测试性和 API 的透明度。
让我们考虑如何通过用 Auryn 将应用连接在一起,来轻松解决一个面向对象 WEB 应用面临的典型问题。这里,我们希望在多个应用层中注入一个单个数据库连接实例。我们有一个控制器类,它请求一个需要 PDO
数据库连接实例的 DataMapper
。
<?php
class DataMapper {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
}
class MyController {
private $mapper;
public function __construct(DataMapper $mapper) {
$this->mapper = $mapper;
}
}
$db = new PDO('mysql:host=localhost;dbname=mydb', 'user', 'pass');
$injector = new Auryn\Injector;
$injector->share($db);
$myController = $injector->make('MyController');
在以上代码中,为 DataMapper
实例提供了我们原来共享的同一 PDO
数据库连接实例。这个例子是特意设计的,并且过于简单,但是含意应该是清晰的:
通过共享类的实例,当为类提供类型提示共享类时,
Auryn\Injector
会始终使用该实例。
一个简单的例子
让我们看一个简单的概念验证:
<?php
class Person {
public $name = 'John Snow';
}
$injector = new Auryn\Injector;
$injector->share('Person');
$person = $injector->make('Person');
var_dump($person->name); // John Snow
$person->name = 'Arya Stark';
$anotherPerson = $injector->make('Person');
var_dump($anotherPerson->name); // Arya Stark
var_dump($person === $anotherPerson); // bool(true) because it's the same instance!
将一个对象定义为共享,会把所提供的实例存储在 Injector 的共享缓存中,将来所有向提供者请求这个类的注入实例,都将返回原来所创建的对象。注意在以上代码中,我们所共享的是类名(Persion
),而不是真正的实例。共享既可作用于类名也可作用于类的实例。不同之处在于当你指定类名时,Injector 会在第一次请求创建共享实例时将其进行缓存。
注意: 一旦 Injector 缓存了共享实例,传递到
Auryn\Injector::make
的实时调用定义将会无效。一旦共享,实例将始终返回其类型的实例化,直到取消共享或刷新对象为止。
实例化委托
通常,工厂类或方法被用于准备实例化后使用的对象。Auryn 允许你通过在每个类的基础上指定可调用的实例化委托,将工厂和构建器直接集成到注入过程中。让我们看一个非常简单的示例来演示注入委托的原理:
<?php
class MyComplexClass {
public $verification = false;
public function doSomethingAfterInstantiation() {
$this->verification = true;
}
}
$complexClassFactory = function() {
$obj = new MyComplexClass;
$obj->doSomethingAfterInstantiation();
return $obj;
};
$injector = new Auryn\Injector;
$injector->delegate('MyComplexClass', $complexClassFactory);
$obj = $injector->make('MyComplexClass');
var_dump($obj->verification); // bool(true)
在上面的代码中我们把 MyComplexClass
的实例 委托给了闭包 $complexClassFactory
。委托一旦创建,Injector 将会在请求实例化 MyComplexClass
时返回指定闭包的结果。
可用的委托类型
可以用 Auryn\Injector::delegate
将任何有效的 PHP Callable 注册成类实例化委托。另外你还能指定委托类的名字,该委托类指定一个 __invoke
方法,它会自动配置,在委托时调用其 __invoke
方法。未实例化的类的实例方法也可以使用 ['NonStaticClassName', 'factoryMethod']
结构来指定。举个例子:
<?php
class SomeClassWithDelegatedInstantiation {
public $value = 0;
}
class SomeFactoryDependency {}
class MyFactory {
private $dependency;
function __construct(SomeFactoryDependency $dep) {
$this->dependency = $dep;
}
function __invoke() {
$obj = new SomeClassWithDelegatedInstantiation;
$obj->value = 1;
return $obj;
}
function factoryMethod() {
$obj = new SomeClassWithDelegatedInstantiation;
$obj->value = 2;
return $obj;
}
}
// 能正常工作,因为 MyFactory 指定了一个魔术方法 __invoke
$injector->delegate('SomeClassWithDelegatedInstantiation', 'MyFactory');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
var_dump($obj->value); // int(1)
// 这样同样能正常工作
$injector->delegate('SomeClassWithDelegatedInstantiation', 'MyFactory::factoryMethod');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
var_dump($obj->value); // int(2)
预备与设值注入
构造函数注入几乎总是比设值注入(Setter Injection)更可取,然而,有些 API 需要额外的实例化后转变。Auryn 利用 Injector::prepare()
方法为这些用例提供了便利。用户可以注册任何类或接口名称以进行实例化后修改。考虑以下情况:
<?php
class MyClass {
public $myProperty = 0;
}
$injector->prepare('MyClass', function($myObj, $injector) {
$myObj->myProperty = 42;
});
$myObj = $injector->make('MyClass');
var_dump($myObj->myProperty); // int(42)
尽管上面的例子是有意设计的,但好处显而易见。
执行注入
除了使用构造函数配置类实例之外,Auryn 还能递归实例化任何有效的 PHP callable 参数。以下示例都可以正常运行:
<?php
$injector = new Auryn\Injector;
$injector->execute(function(){});
$injector->execute([$objectInstance, 'methodName']);
$injector->execute('globalFunctionName');
$injector->execute('MyStaticClass::myStaticMethod');
$injector->execute(['MyStaticClass', 'myStaticMethod']);
$injector->execute(['MyChildStaticClass', 'parent::myStaticMethod']);
$injector->execute('ClassThatHasMagicInvoke');
$injector->execute($instanceOfClassThatHasMagicInvoke);
$injector->execute('MyClass::myInstanceMethod');
另外,你可以为非静态方法传入类名,在提供和调用指定方法前,注入器将会自动提供该类的实例(受已被注入器存储的任何定义或共享实例的约束):
<?php
class Dependency {}
class AnotherDependency {}
class Example {
function __construct(Dependency $dep){}
function myMethod(AnotherDependency $arg1, $arg2) {
return $arg2;
}
}
$injector = new Auryn\Injector;
// 输出:int(42)
var_dump($injector->execute('Example::myMethod', $args = [':arg2' => 42]));
依赖解析
Auryn\Injector
按照如下顺序解析依赖:
- 如果存在一个相关类的共享实例,则总是返回该共享实例。
- 如果将一个 callable 委托分配给了类,则总会使用它们返回结果。
- 如果一个实时调用定义被传给
Auryn\Injector::make
,将使用该定义 - 如果一个预定义的定义存在,将使用它
- 如果一个依赖是类型提示,注入器将更具任何实现或定义递归实例它
- 如果不存在类型提示并且该参数具有默认值,则注入默认值
- 如果定义了一个全局参数值,则使用该值
- 因为你做的一些蠢事而引发的异常
用例示例
在 PHP 社区依赖注入容器(DIC)通常被误解。罪魁祸首之一是主流应用框架对这类容器的滥用。经常的,这些框架将它们的 DIC 扭曲为服务定位器(Service Locator)反面模式。这很可惜,因为一个良好的 DIC 与服务定位器完全相反。
Auryn 不是一个服务定位器!
使用 DIC 连结你的应用相对于将 DIC 作为对象的依赖项传递(服务器定位器)有诸多不同。服务定位器(SL)是一个反面模式,它隐藏了类的依赖关系,使得代码难以维护,并且会欺骗你的 API。
当你向构造函数传递了 SL,会导致很难确定类的真实依赖项是什么。House
对象依赖 Door
和 Window
对象。House
对象不会依赖 ServiceLocator
实例,不论 ServiceLocatior
能否提供 Door
和 Window
对象。
在现实生活中你不会(希望)把整个五金店运到工地上来盖房,因此你可以根据需要获取任何部件。同样的,工头(__construct()
)询问将要用到的特定部件(Door
和 window
)然后进行采购。你的对象应该以同样的方式起作用;它们应该只请求完成他们的工作所需要的特定依赖项。赋予 House
访问整个五金店的权限,最好的情况是糟糕的 OPP 风格,最差的情况就是可维护性的噩梦。这里的要点是:
重要: 不要像服务定位器那样使用 Auryn
避免讨厌的单例
Web 应用的一个常见麻烦是限制数据库连接实例的数量。每次我们需要与数据库对话时都打开一个新的链接既慢又浪费。很不幸,使用 Singleton 来限制这些实例使得代码既脆弱又难以测试。让我们看看如何使用 Auryn 在整个应用范围内注入相同的 PDO
实例。
假如我们有一个服务类,它需要两个独立的数据映射器才能将信息持久化到数据库。
<?php
class HouseMapper {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
public function find($houseId) {
$query = 'SELECT * FROM houses WHERE houseId = :houseId';
$stmt = $this->pdo->prepare($query);
$stmt->bindValue(':houseId', $houseId);
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Model\\Entities\\House');
$stmt->execute();
$house = $stmt->fetch(PDO::FETCH_CLASS);
if (false === $house) {
throw new RecordNotFoundException(
'No houses exist for the specified ID'
);
}
return $house;
}
// 这里是更多数据映射器方法
}
class PersonMapper {
private $pdo;
public function __construct(PDO $pdo) {
$this->pdo = $pdo;
}
// 这里是数据映射器方法
}
class SomeService {
private $houseMapper;
private $personMapper;
public function __construct(HouseMapper $hm, PersonMapper $pm) {
$this->houseMapper = $hm;
$this->personMapper = $pm;
}
public function doSomething() {
// 用映射器做些什么
}
}
在我们的连接代码中,我们只实例化了一次 PDO
实例,并且将其在 Injector
的上下文中共享:
<?php
$pdo = new PDO('sqlite:some_sqlite_file.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$injector = new Auryn\Injector;
$injector->share($pdo);
$mapper = $injector->make('SomeService');
在以上代码中,DIC 实例化了我们的服务类。更重要的是,为此而生成的数据映射器类将注入我们起初共享的同一个数据库连接实例。
当然,我们不必手动实例化我们的 PDO
实例。我们只需要轻松地向容器植入一个定义,它负责如何创建 PDO
对象,并为我们处理事情:
<?php
$injector->define('PDO', [
':dsn' => 'sqlite:some_sqlite_file.db'
]);
$injector->share('PDO');
$service = $injector->make('SomeService');
在以上代码中,注入器将传送字符串定义作为在 PDO::__construct
方法中的 $dsn
参数,然后只在实例化类之一需要 PDO
实例的情况下,才自动生成共享的 PDO
实例!
引导应用程序
DIC 应该用来把你应用程序中不同的对象连接在一起,形成有结合力的功能单元(一般在应用程序启动或前控制器阶段)。这样一种用法为面相对象(OO)Web 应用程序中的棘手问题之一提供了一种优雅的解决方案:如何在事先依赖关系未知的路由环境中实例化类。
考虑如下前控制器代码的任务:
- 加载应用程序路由列表,并将它们传递给路由器
- 生成客户端的 HTTP 请求模型
- 将请求实例路由到给定的应用程序路由列表
- 实例化路由的控制器并调用一个适用于 HTTP 请求的方法
<?php
define('CONTROLLER_ROUTES', '/hard/path/to/routes.xml');
$routeLoader = new RouteLoader();
$routes = $routeLoader->loadFromXml(CONTROLLER_ROUTES);
$router = new Router($routes);
$requestDetector = new RequestDetector();
$request = $requestDetector->detectFromSuperglobal($_SERVER);
$requestUri = $request->getUri();
$requestMethod = strtolower($request->getMethod());
$injector = new Auryn\Injector;
$injector->share($request);
try {
if (!$controllerClass = $router->route($requestUri, $requestMethod)) {
throw new NoRouteMatchException();
}
$controller = $injector->make($controllerClass);
$callableController = array($controller, $requestMethod);
if (!is_callable($callableController)) {
throw new MethodNotAllowedException();
} else {
$callableController();
}
} catch (NoRouteMatchException $e) {
// 发送 404 响应
} catch (MethodNotAllowedException $e) {
// 发送 405 响应
} catch (Exception $e) {
// 发送 500 响应
}
在其它地方,我们都有各式各样的控制器类,每个类请求各自独立的依赖项:
<?php
class WidgetController {
private $request;
private $mapper;
public function __construct(Request $request, WidgetDataMapper $mapper) {
$this->request = $request;
$this->mapper = $mapper;
}
public function get() {
// 为 HTTP GET 请求执行的操作
}
public function post() {
// 为 HTTP POST 请求执行的操作
}
}
在以上示例中,Auryn DIC 允许我们编写完全可测试的、完全面向对象的控制器,以请求他们的依赖项。因为 DIC 递归实例化它所创建对象的依赖关系,因此我们不需要到处传递服务定位器(Srvice Locator)。另外,这个示例展示了我们如何使用 Auryn DIC 的共享功能消灭讨厌的单例现象。在前控制器代码中,我们共享了请求对象,因此用 Auryn\Injector
实例化的任何类,在请求 Request
时都会接收相同的实例。这个特性不仅有助于消除单例现象,也不再需要难以测试的 static
属性。