danack/injector

Danack/Injector 是一个递归自动装配的依赖注入器。

0.5.0 2023-04-29 17:28 UTC

This package is auto-updated.

Last update: 2024-09-21 00:19:25 UTC


README

Danack/Injector 是一个递归依赖注入器。使用它来自动装配和连接 S.O.L.I.D.、面向对象的 PHP 应用程序。

其他分支

原始库 rdlowrey/auryn 处于低维护模式。也就是说,新功能很难添加,且不支持新版本 PHP 的新发布可能无法及时完成。为什么某些功能没有添加到 Auryn 的说明见 此处

其他类似库可在以下位置找到:

  • martin-hughes/auryn 是从这个仓库的分叉,并维护当前命名空间和接口。它不太可能引入新的重要功能,而是专注于错误修复和测试。

  • overclokk/auryn 是从这个仓库的分叉,并维护当前命名空间和接口。它增加了使用 Ocramius/ProxyManager 懒惰实例化依赖项的功能。

  • amphp/injector 是使用新命名空间和略有不同接口的重写版本,需要您更新代码。它将引入新功能,并且随着时间的推移与这个仓库有所偏离。

工作原理

auryn 会在许多其他方面递归地根据类构造函数签名中指定的参数类型提示实例化类依赖项。这需要使用反射。您可能听说过“反射很慢”。让我们澄清一下:如果做错了,“任何东西”都可能“很慢”。反射比磁盘访问快一个数量级,比从远程数据库检索信息(例如)快几个数量级。此外,每个反射都提供了缓存结果的机会,如果您担心速度。auryn 缓存它生成的任何反射,以最大限度地减少潜在的性能影响。

auryn 不是 服务定位器。不要通过将注入器传递到您的应用程序类中来将其变成服务定位器。服务定位器是一个反模式;它隐藏类依赖项,使代码更难维护,并使您的 API 成为谎言!您应该 在引导阶段使用注入器将应用程序的不同部分连接起来。

指南

基本用法

高级用法

示例用例

需求和安装

  • Danack/Injector 需要 PHP 7.2 或更高版本。

安装

Github

您可以从 github 仓库随时克隆最新的 Danack/Injector 版本。

$ git clone git://github.com/danacj/injector.git
Composer

您还可以使用 composer 在您的项目 composer.json 中将 auryn 包含为依赖项。相关的包是 rdlowrey/auryn

或者使用 composer 命令行工具安装该包

composer require danack/injector
手动下载

存档的标记版本也可以在项目的标签页手动下载

运行测试

为了允许在所有支持的PHP版本上安装合适的PHPUnit版本,而不是直接依赖于PHPUnit,Auryn选择依赖simple-phpunit。

执行composer update后,需要告诉simple-phpunit安装PHPUnit

vendor/bin/simple-phpunit install

vendor/bin/simple-phpunit --version

然后可以使用以下命令运行测试

vendor/bin/simple-phpunit

基本用法

要开始使用注入器,只需创建一个新的Auryn\Injector类实例(即“注入器”)

<?php
$injector = new DI\Injector;

基本实例化

如果一个类在其构造函数签名中没有指定任何依赖项,那么使用注入器生成它就没有太多意义。然而,为了完整性考虑,可以考虑以下操作,以获得等效结果

<?php
$injector = new DI\Injector;
$obj1 = new SomeNamespace\MyClass;
$obj2 = $injector->make('SomeNamespace\MyClass');

var_dump($obj2 instanceof SomeNamespace\MyClass); // true
具体类型提示的依赖项

如果一个类只要求具体依赖项,可以使用注入器注入它们,而不需要指定任何注入定义。例如,在以下场景中,可以使用注入器自动为MyClass提供所需的SomeDependencyAnotherDependency类实例

<?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 DI\Injector;
$myObj = $injector->make('MyClass');

var_dump($myObj->dep1 instanceof SomeDependency); // true
var_dump($myObj->dep2 instanceof AnotherDependency); // true
递归依赖实例化

注入器的一个关键属性是它递归遍历类依赖树来实例化对象。这只是一个花哨的说法,“如果你实例化了对象A,它请求对象B,注入器将实例化对象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 DI\Injector;
$car = $injector->make('Car');
var_dump($car instanceof Car); // true

注入定义

你可能已经注意到,之前的示例都展示了使用显式、类型提示的具体构造函数参数来实例化类的操作。显然,许多类都不会符合这种模式。一些类会类型提示接口和抽象类。一些会指定标量参数,这些参数在PHP中没有类型提示的可能性。其他参数将是数组等。在这种情况下,我们需要通过告诉注入器我们确切想要注入的内容来帮助注入器。

定义构造函数参数的类名

让我们看看如何为具有构造函数签名中非具体类型提示的类提供支持。考虑以下代码,其中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 DI\Injector;
$injector->define('Car', ['engine' => 'V8']);
$car = $injector->make('Car');

var_dump($car instanceof Car); // true

这里最重要的几点是

  1. 自定义定义是一个array,其键与构造函数参数名称匹配
  2. 定义数组中的值表示要注入指定参数键的类名称

由于我们需要定义的Car构造函数参数名为$engine,我们的定义指定了一个engine键,其值为要注入的类名(V8)。

自定义注入定义仅在每参数基础上是必要的。例如,在以下类中,我们只需要定义$arg2的可注入类,因为$arg1指定了具体的类类型提示

<?php
class MyClass {
    private $arg1;
    private $arg2;
    public function __construct(SomeConcreteClass $arg1, SomeInterface $arg2) {
        $this->arg1 = $arg1;
        $this->arg2 = $arg2;
    }
}

$injector = new DI\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 DI\Injector;
$dependencyInstance = new SomeImplementation;
$injector->define('MyClass', [':dependency' => $dependencyInstance]);

$myObj = $injector->make('MyClass');

var_dump($myObj instanceof MyClass); // true

注意:由于这个define()调用传递的是原始值(正如冒号:的使用所示),你可以通过省略数组键并依赖于参数顺序而不是名称来实现相同的结果。如下所示:$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 DI\Injector;
$myObj = $injector->make('MyClass', ['dependency' => 'SomeImplementationClass']);

var_dump($myObj instanceof MyClass); // true

上述代码演示了即使我们没有调用注入器的 define 方法,调用时指定仍然允许我们实例化 MyClass

注意:即时实例化定义将覆盖指定类预先定义的定义,但仅限于 Auryn\Injector::make 的那个特定调用上下文中。

类型提示别名

面向接口是面向对象设计(OOD)中最有用的概念之一,并且代码应该尽可能的类型提示接口。但这是否意味着我们必须为应用中的每个类分配注入定义以获得抽象依赖的好处?幸运的是,这个问题的答案是,“不”。注入器通过接受“别名”来实现这一目标。考虑以下示例:

<?php
interface Engine {}
class V8 implements Engine {}
class Car {
    private $engine;
    public function __construct(Engine $engine) {
        $this->engine = $engine;
    }
}

$injector = new DI\Injector;

// Tell the Injector class to inject an instance of V8 any time
// it encounters an Engine type-hint
$injector->alias('Engine', 'V8');

$car = $injector->make('Car');
var_dump($car instanceof Car); // bool(true)

在这个示例中,我们演示了如何为特定接口或抽象类类型提示的任何出现指定别名类。一旦分配了实现,注入器将使用它为具有匹配类型提示的任何参数提供实例。

重要:如果为实施分配覆盖的参数定义了注入定义,则定义优先于实施。

非类参数

所有之前的示例都演示了注入器类如何根据类型提示、类名定义和现有实例来实例化参数。但如果我们想将标量或其他非对象变量注入到类中会发生什么呢?首先,让我们建立以下行为规则:

重要:注入器默认假定所有命名参数定义是类名。

如果您希望注入器将命名参数定义视为“原始”值而不是类名,您必须在定义中用冒号字符 : 前缀参数名称。例如,考虑以下代码,我们告诉注入器共享一个 PDO 数据库连接实例并定义其标量构造参数:

<?php
$injector = new DI\Injector;
$injector->share('PDO');
$injector->define('PDO', [
    ':dsn' => 'mysql:dbname=testdb;host=127.0.0.1',
    ':username' => 'dbuser',
    ':passwd' => 'dbpass'
]);

$db = $injector->make('PDO');

参数名称前的冒号字符告诉注入器,相关的值不是类名。如果省略了上面的冒号,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 DI\Injector;
$injector->defineParam('myValue', $myUniversalValue);
$obj = $injector->make('MyClass');
var_dump($obj->myValue === 42); // bool(true)

因为我们为 myValue 指定了全局定义,所以所有不按某种方式定义(如下所示)且与指定参数名称匹配的参数都将自动填充全局值。如果参数符合以下任何标准,则不使用全局值:

  • 参数类型
  • 预定义的注入定义
  • 自定义调用时定义

高级用法

实例共享

现代面向对象编程中常见的一种反模式是Singleton。许多开发者为了限制类只能有一个实例,往往会误用static Singleton实现来创建配置类和数据库连接等。虽然防止类的多个实例是必要的,但Singleton方法却会导致测试性下降,因此通常应避免使用。Auryn\Injector允许在不同的上下文中轻松共享类实例,同时最大程度地提高测试性和API透明度。

让我们看看如何使用auryn将应用程序连接起来,从而轻松解决面向对象Web应用程序中常见的典型问题。在这里,我们希望在一个应用程序的多个层次中注入单个数据库连接实例。我们有一个控制器类,它请求一个需要PDO数据库连接实例的数据映射器。

<?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 DI\Injector;
$injector->share($db);

$myController = $injector->make('MyController');

在上面的代码中,DataMapper实例将被分配与最初共享的相同的PDO数据库连接实例。这个例子虽然设计得很简单,但其含义应该是明确的。

通过共享一个类的实例,Auryn\Injector在提供类实例时,将始终使用那个实例。

一个更简单的例子

让我们看看一个简单的概念证明。

<?php
class Person {
    public $name = 'John Snow';
}

$injector = new DI\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!

将一个对象定义为共享将把提供的实例存储在注入器的共享缓存中,并且将来对该提供器请求该类注入实例的所有后续请求都将返回最初创建的对象。请注意,在上面的代码中,我们共享了类名(Person)而不是实际的实例。共享可以使用类名或类的实例。区别在于,当指定类名时,注入器将在第一次请求创建它时缓存共享的实例。

注意:一旦注入器缓存了一个共享实例,传递给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 DI\Injector;
$injector->delegate('MyComplexClass', $complexClassFactory);

$obj = $injector->make('MyComplexClass');
var_dump($obj->verification); // bool(true)

在上面的代码中,我们将MyComplexClass类的实例化委托给了闭包$complexClassFactory。一旦完成委托,注入器将在被要求实例化MyComplexClass时返回指定的闭包的结果。

可用的代理类型

任何有效的PHP可调用都可以使用Auryn\Injector::delegate注册为类实例化代理。此外,您可以指定一个代理类的名称,该代理类指定了一个__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;
    }
}

// Works because MyFactory specifies a magic __invoke method
$injector->delegate('SomeClassWithDelegatedInstantiation', 'MyFactory');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
var_dump($obj->value); // int(1)

// This also works
$injector->delegate('SomeClassWithDelegatedInstantiation', 'MyFactory::factoryMethod');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
$obj = $injector->make('SomeClassWithDelegatedInstantiation');
var_dump($obj->value); // int(2)

准备和设置器注入

构造器注入几乎总是优于设置器注入。然而,某些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)

虽然上面的例子是虚构的,但其用途应该是明显的。

此外,准备方法能够用相同或派生类型的另一个对象替换正在准备的对象

<?php

class FooGreeter {
    public function getMessage(): string {
        return "Hello, I am foo.";
    }
}

class BarGreeter extends FooGreeter {
    public function getMessage(): string {
        return "Hello, I am bar.";
    }
}

$injector = new \DI\Injector();

$injector->prepare(FooGreeter::class, function($myObj, $injector) {
    return new BarGreeter();
});

$myObj = $injector->make(FooGreeter::class);
echo $myObj->getMessage(); // Output is: "Hello, I am bar."

这种用途的实用性不是很清楚。

返回的任何不是相同或派生类型的值都将被忽略。

执行时注入

除了使用构造函数创建类实例外,auryn 还可以递归地实例化任何有效的 PHP 可调用对象的参数。以下示例均有效:

<?php
$injector = new DI\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 DI\Injector;

// outputs: int(42)
var_dump($injector->execute('Example::myMethod', $args = [':arg2' => 42]));

Injector::make 和 Injector::execute 自定义参数

在 Injector::make($name, array $args = array()) 和 Injector::execute($callableOrMethodStr, array $args = array())) 中的 args 参数允许您传递一组定制的参数,这些参数将在创建/执行过程中使用。

如何使用这些注入器参数的规则如下。

假设有一个名为 'foo'、位于 'i' 位置的参数,其类型为 'bar',用于创建/执行的对象

  1. 如果存在整数索引键 'i'(即 $args[$i] 是否存在?),则直接使用 $args[$i] 的值作为该参数。

  2. 如果存在字符串索引键 'foo'(即 $args['foo'] 是否存在?),则使用 $args['foo'] 的值作为该参数。

  3. 如果存在字符串索引键 Injector::A_DELEGATE . 'foo'(即 $args['+foo'] 是否存在?),则将 $args['+' . $i] 解释为要调用的代理可调用者,并使用其返回值作为该参数。

  4. 如果存在字符串索引键 Injector::A_DEFINE . 'foo'(即 $args['@foo'] 是否存在?),则将 $args['+' . $i] 解释为一个数组

$params = [
    PrefixDefineDependency::class,
    [Injector::A_RAW . 'message' => $message]
];

$object = $injector->make(
    PrefixDefineTest::class,
    [Injector::A_DEFINE . 'pdd' => $params]
);

例如,当注入器创建名为 'PrefixDefineTest' 的类时,该类依赖于名为 PrefixDefineDependency 的类,该类在构造函数中作为参数 'pdd',则使用数组 $params[1] 中的值来实例化 PrefixDefineDependency 类。

  1. 如果存在字符串索引键 Injector::A_DEFINE . '+foo'(即 $args[':foo'] 是否存在?),则将 $args['+' . $i] 解释为用于定义名称的值。这与 $injector->define('foo', 'bar'); 的行为类似

  2. 尝试通过正常的 Auryn 参数构建过程构建参数。

依赖项解析

Auryn\Injector 按以下顺序解决依赖关系

  1. 如果存在该类的共享实例,则始终返回该共享实例
  2. 如果为类分配了代理可调用者,则其返回结果将始终被使用
  3. 如果将调用时定义传递给 Auryn\Injector::make,则使用该定义
  4. 如果存在预定义的定义,则使用它
  5. 如果依赖项被类型提示,则注入器将递归地实例化它,受任何实现或定义的限制
  6. 如果没有类型提示,且参数有默认值,则注入默认值
  7. 如果定义了全局参数值,则使用该值
  8. 抛出异常,因为您做了愚蠢的事情

示例用例

依赖注入容器(DIC)在 PHP 社区中被普遍误解。主要原因是主流应用程序框架中此类容器的不当使用。通常,这些框架将它们的 DIC 转变为服务定位器反模式。这是一个遗憾,因为一个好的 DIC 应该与服务定位器正好相反。

auryn 不是服务定位器!

使用 DIC 来连接应用程序与将 DIC 作为依赖项传递给对象(服务定位器)之间有很大的不同。服务定位器(SL)是一个反模式——它隐藏类依赖关系,使代码难以维护,并使您的 API 成为谎言。

当您将SL传递给构造函数时,很难确定类的实际依赖关系。一个House对象依赖于DoorWindow对象。一个House对象不管ServiceLocator能否提供DoorWindow对象,都不会依赖于ServiceLocator的实例。

在现实生活中,您不会把整个建材商店(希望如此)运到建筑工地,以便访问所需的任何部分。相反,工头(__construct())会询问所需的具体部分(DoorWindow)并着手采购。您的对象应该以同样的方式运作;它们应该只为完成工作所需的特定依赖项而请求。给予House整个建材商店的访问权限,最多是糟糕的面向对象(OOP)风格,最糟糕的是维护噩梦。这里的要点是

重要:不要像服务定位器一样使用auryn!

避免邪恶的单例

在Web应用程序中,限制数据库连接实例的数量是一个常见的困难。每次我们需要与数据库通信时打开新的连接都是浪费且缓慢的。不幸的是,使用单例来限制这些实例会使代码脆弱且难以测试。让我们看看我们如何使用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;
    }

    // more data mapper methods here ...
}

class PersonMapper {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    // data mapper methods here
}

class SomeService {
    private $houseMapper;
    private $personMapper;
    public function __construct(HouseMapper $hm, PersonMapper $pm) {
        $this->houseMapper = $hm;
        $this->personMapper = $pm;
    }
    public function doSomething() {
        // do something with the mappers
    }
}

在我们的配置/引导代码中,我们只需实例化一次PDO实例,并在Injector的上下文中共享它

<?php
$pdo = new PDO('sqlite:some_sqlite_file.db');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$injector = new DI\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实例,注入器将自动生成共享的PDO实例,并将字符串定义作为$dsn参数传递给PDO::__construct方法

应用程序引导

DIC应用于将应用程序的不同对象连接成一个功能单元(通常在引导或前端控制器阶段)。这种使用方式提供了一种优雅的解决方案来解决面向对象(OO)Web应用程序中的一个棘手问题:在依赖关系无法提前知晓的路由环境中如何实例化类。

考虑以下前端控制器代码,其任务是

  1. 加载应用程序路由列表并将其传递给路由器
  2. 生成客户端HTTP请求的模型
  3. 根据应用程序的路由列表路由请求实例
  4. 实例化路由控制器并调用与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 DI\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) {
    // send 404 response
} catch (MethodNotAllowedException $e) {
    // send 405 response
} catch (Exception $e) {
    // send 500 response
}

并且我们还拥有各种控制器类,每个类都要求自己的特定依赖项

<?php

class WidgetController {
    private $request;
    private $mapper;
    public function __construct(Request $request, WidgetDataMapper $mapper) {
        $this->request = $request;
        $this->mapper = $mapper;
    }
    public function get() {
        // do something for HTTP GET requests
    }
    public function post() {
        // do something for HTTP POST requests
    }
}

在上面的示例中,auryn DIC 允许我们编写完全可测试的、完全面向对象的控制器,这些控制器请求它们的依赖项。由于 DIC 递归地实例化它所创建的对象的依赖项,所以我们不需要传递 Service Locator。此外,此示例还展示了如何使用 auryn DIC 的共享功能消除恶意的 Singleton。在前控制器代码中,我们共享请求对象,以便任何由 Auryn\Injector 实例化的、请求 Request 的类都将接收到相同的实例。此功能不仅有助于消除 Singleton,还有助于消除难以测试的 static 属性。

当 Auryn 的应用引导不可行时

有时,应用程序的初始化不在你的控制之下。一个例子是编写 WordPress 插件,其中 WordPress 初始化你的插件,而不是反过来。

你仍然可以使用 Auryn,通过使用一个函数来创建注入器的一个实例

function getInjector()
{
    static $injector = null;
	if ($injector == null) {
		$injector = new \DI\Injector();
		// Do injector defines/shares/aliases/delegates here
	}

    return $injector;
}

高级模式

“可变参数”依赖项

有时你的代码可能需要将可变数量的对象作为参数传递。

class Foo {
  public function __construct(Repository ...$repositories) {
  // do stuff with $repositories
  }
}

在这种情况下,$repositories 不代表一个简单的变量,而是代表一个复杂类型。

由于 Auryn 通过定义类型规则来工作,所以 Auryn 无法执行注入,因此你需要使用更高级的技术来执行注入。

使用代理函数的可变参数

支持能够创建具有可变依赖项的对象的最简单方法是使用一个代理函数来创建它

function createFoo(RepositoryLocator $repoLocator)
{
    // Or whatever code is needed to find the repos.
    $repositories = $repoLocator->getRepos('Foo');

    return new Foo($repositories);
}

$injector->delegate('Foo', 'createFoo');

编写代码可能只需几分钟,但缺点是将一些应用程序逻辑移入注入器。

使用工厂类的可变参数

创建具有可变依赖项的对象的另一种稍微长一点的方法是将它们重构为使用工厂对象来获取依赖项

class RepositoryList
{
    /**
    * @return Repository[]
    */
    public function getRelevantRepositories() {
        // do stuff with $repositories
    }
}

class Foo {
    public function __construct(RepositoryList $respositoryList)
    {
        $repositories = $respositoryList->getRelevantRepositories();

        // error handling goes here

        // do stuff with $repositories
    }
}

这可能比使用代理方法略好一些,因为它避免了业务/应用程序逻辑在依赖注入器中,并在你的代码中为你提供了一个适当的错误处理位置。

上下文对象和同一类型的多个实例

有时你可能需要具有同一类型的多个实例。

例如,一个将数据从实时数据库移动到存档数据库的背景作业可能需要注入 DB 类的两个实例

class DataArchiver
{
    public function __construct(private PDO $live_db, private PDO $archive_db)
    {
    }
}

这可以通过使用类型系统来创建更具体的类型来规避

class LivePDO extends PDO {}
class ArchivePDO extends PDO {}

class DataArchiver
{
    public function __construct(private LivePDO $live_db, private ArchivePDO $archive_db)
    {
    }
}

更具体的类型可以通过为每个类型配置适当的代理函数在 Auryn 中创建

这种方法可行,并且实际上对于小型项目来说是一个合理的做法,但对于大型项目来说,有一个更全面的方法更合适。

封装的上下文

或者更准确地说是使用“封装上下文模式”](https://www.allankelly.net/static/patterns/encapsulatecontext.pdf).

“封装上下文”的简短描述是创建特定的类型,它包含特定业务/域问题所需的所有类型,并允许你将它们具体连接起来

class DataArchiverContext
{
    public function __construct(
        private PDO $live_db,
        private PDO $archive_db
    ) {

    public function get_live_db(): PDO
    {
        return $this->live_db;
    }

    public function get_archive_db(): PDO
    {
        return $this->archive_db;
    }
}

class DataArchiver
{
    public function __construct(private DataArchiverContext $dac)
    {
    }
}

function createDataArchiver()
{
    return new DataArchiver(
        createLiveDB(),
        createArchiveDB()
    );
}

$injector->delegate(DataArchiverContext::class, 'createDataArchiver');

封装上下文使你的代码更容易理解。你可以看到

  • 特定上下文的使用位置。
  • 其中包含的类型。
  • 它的创建方式,包括任何特别的规则。

这使得维护和推理大型程序更容易。

运行测试和基准

运行测试

由于没有单个版本的 PHPUnit 可以在 Auryn 支持的所有 PHP 版本上运行,我们使用 simple-phpunit 来安装适当的 PHP 版本。

在运行 composer update 获取最新依赖项后,运行

php vendor/bin/simple-phpunit install

以使 simple-phpunit 安装 PHPUnit。然后可以使用以下命令运行测试

php vendor/bin/simple-phpunit

simple-phpunit 接受 PHPUnit 命令行选项,并将它们传递给 PHPUnit,例如 php php vendor/bin/simple-phpunit --group wip 以仅运行标记为属于“wip”组的测试。

运行基准测试

我们使用PHPBench来允许检查代码更改时的性能提升/退化。使用它的最简单方法是:

  1. 通过以下命令创建基准基线:
vendor/bin/phpbench run --tag=benchmark_original --retry-threshold=5 --iterations=10
  1. 应用您的代码更改。

  2. 运行基准测试,并通过以下命令将结果与 'benchmark_original' 进行比较:

vendor/bin/phpbench run --report=aggregate --ref=benchmark_original --retry-threshold=5 --iterations=10