cmpayments/atreyu

Atreyu 是一个用于初始化面向对象 PHP 应用程序的依赖注入器。

v1.6.0 2017-02-08 16:18 UTC

This package is auto-updated.

Last update: 2024-08-27 00:58:05 UTC


README

License Latest Stable Version Scrutinizer Code Quality Total Downloads Reference Status

Atreyu 是 Auryn 的扩展,其诞生源于在使用类型提示参数实例化类时的需求。如果没有类型提示,也可以使用 docblock 来识别参数(仅作为后备方案)。

Atreyu 与 Auryn 的主要区别在于 Atreyu 会检查类型提示的方法参数。如果方法参数没有类型提示,Atreyu 将检查 docblock。

Atreyu 是基于 Auryn 的递归依赖注入器。使用 Atreyu 引入并连接 S.O.L.I.D.、面向对象 PHP 应用程序。

工作原理

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

Atreyu 不是 服务定位器。不要通过将注入器传递到你的应用类中将其变成服务定位器。服务定位器是一个反模式;它隐藏类依赖关系,使代码更难维护,并使你的 API 造假!你应该 在引导阶段使用注入器来连接你的应用的不同部分。

指南

基本用法

高级用法

示例用例

要求和安装

  • Atreyu 需要 PHP 5.4 或更高版本。

安装

Github

你可以从 github 仓库随时克隆最新的 Atreyu 版本

$ git clone git://github.com/cmpayments/atreyu.git
Composer

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

或者使用 composer 命令行工具要求包

composer require cmpayments/atreyu
手动下载

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

基本用法

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

<?php
$injector = new Atreyu\Injector;

基本实例化

如果一个类在其构造函数签名中没有指定任何依赖关系,使用注入器生成它的意义不大。然而,为了完整性考虑,以下是一些等效的操作

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

在这种情况下,我们只需要提前为类定义一个注入定义

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

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

var_dump($myObj instanceof MyClass); // true

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

动态指定注入定义

你还可以使用Atreyu\Injector::make在调用时指定注入定义

<?php
interface SomeInterface {}

class SomeImplementationClass implements SomeInterface {}

class MyClass {
    private $dependency;
    public function __construct(SomeInterface $dependency) {
        $this->dependency = $dependency;
    }
}

$injector = new Atreyu\Injector;
$myObj = $injector->make('MyClass', ['dependency' => 'SomeImplementation']);

var_dump($myObj instanceof MyClass); // true

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

注意:动态实例化定义将覆盖指定的类的预定义定义,但仅在该特定调用Atreyu\Injector::make的上下文中。

类型提示别名

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

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

$injector = new Atreyu\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 Atreyu\Injector;
$injector->share('PDO');
$injector->define('PDO', [
    ':dsn' => 'mysql:dbname=testdb;host=127.0.0.1',
    ':username' => 'dbuser',
    ':passwd' => 'dbpass'
]);

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

参数名之前的冒号字符告诉注入器关联的值不是类名。如果省略了上面的冒号,Atreyu将尝试实例化指定字符串中名称的类,并引发异常。此外,请注意,我们还可以在上面的定义中指定数组、整数或任何其他数据类型。只要参数名前有:前缀,Atreyu就会直接注入值,而不会尝试实例化它。

注意:如前所述,由于此define()调用正在传递原始值,您可以选择按参数顺序分配值而不是按名称分配。由于PDO的前三个参数是$dsn$username$password,按此顺序,您可以通过省略数组键来达到相同的效果,如下所示:$injector->define('PDO', ['mysql:dbname=testdb;host=127.0.0.1', 'dbuser', 'dbpass']);

全局参数定义

有时应用程序可能需要在每个地方都重用相同的值。然而,在可能被使用的每个地方手动指定此类定义可能会很麻烦。Atreyu通过公开Injector::defineParam()方法来减轻这个问题。考虑以下示例...

<?php
$myUniversalValue = 42;

class MyClass {
    public $myValue;
    public function __construct($myValue) {
        $this->myValue = $myValue;
    }
}

$injector = new Atreyu\Injector;
$injector->defineParam('myValue', $myUniversalValue);
$obj = $injector->make('MyClass');
var_dump($obj->myValue === 42); // bool(true)

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

  • 类型提示
  • 预定义的注入定义
  • 自定义调用时定义

高级用法

实例共享

现代面向对象编程中更为普遍的祸害之一是Singleton反模式。希望限制类只有一个实例的编码人员往往会陷入使用static Singleton实现配置类和数据库连接等事物的陷阱。虽然通常有必要防止类的多个实例,但Singleton方法会扼杀可测试性,通常应避免使用。Atreyu\Injector使在上下文中共享类实例变得简单,同时允许最大的可测试性和API透明度。

让我们考虑一下,如何通过使用Atreyu将应用程序连接起来,轻松解决面向对象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 Atreyu\Injector;
$injector->share($db);

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

在上面的代码中,DataMapper实例将配备与最初共享的相同的PDO数据库连接实例。这个例子是人为的,过于简单,但其含义应该是清晰的

通过共享一个类的实例,Atreyu\Injector将在提供具有共享类类型提示的类时始终使用该实例。

一个更简单的例子

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

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

$injector = new Atreyu\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),而不是实际实例。共享可以使用类名或类的实例。区别在于,当您指定类名时,注入器将在第一次被要求创建它时缓存共享实例。

注意:一旦注入器缓存了共享实例,传递给 Atreyu\Injector::make 的调用时间定义将没有任何效果。一旦共享,实例将始终返回其类型的实例化,直到对象取消共享或刷新。

实例化代理

通常使用工厂类/方法来准备对象以便实例化后使用。Atreyu 允许您通过在每个类的基础上指定可调用实例化代理来直接将工厂和构建器集成到注入过程中。让我们通过一个非常基本的示例来展示注入代理的概念。

<?php
class MyComplexClass {
    public $verification = false;
    public function doSomethingAfterInstantiation() {
        $this->verification = true;
    }
}

$complexClassFactory = function() {
    $obj = new MyComplexClass;
    $obj->doSomethingAfterInstantiation();

    return $obj;
};

$injector = new Atreyu\Injector;
$injector->delegate('MyComplexClass', $complexClassFactory);

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

在上面的代码中,我们将 MyComplexClass 类的实例化委托给了闭包 $complexClassFactory。一旦做出这个委托,当请求实例化 MyComplexClass 时,注入器将返回指定闭包的结果。

可用的代理类型

任何有效的 PHP 可调用都可以使用 Atreyu\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 需要额外的实例化后突变。Atreyu 通过其 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)

虽然上面的例子是人为的,但其有用性应该是明显的。

执行注入

除了使用构造函数提供类实例之外,Atreyu 还可以递归实例化任何有效的 PHP 可调用 的参数。以下所有示例都有效

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

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

依赖解析

Atreyu\Injector 按以下顺序解决依赖项

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

Auryn 之上的依赖解析功能

<?php

namespace Atreyu\Test;

use Atreyu\Injector;

interface DepInterface
{
}


class TestDependency
{
    public $testProp = 'testVal';
}


class TestDependency2 extends TestDependency implements DepInterface
{
    public $testProp = 'testVal2';
}


class TestMultiDepsNeeded
{
    public function __construct(TestDependency $val1, DepInterface $val2)
    {
        $this->testDep  = $val1;
        $this->testDep2 = $val2;
    }
}


class TestMultiDepsNeededExample2
{
    /**
     * TestMultiDepsNeededExample2 constructor.
     *
     * @param TestDependency $val1
     * @param DepInterface   $val2
     *
     * @throws \Exception
     */
    public function __construct($val1, $val2)
    {
        if (!($val1 instanceof TestDependency)) {
            throw new \Exception('param $val1 does not meet required input');
        }

        if (!($val2 instanceof TestDependency2)) {
            throw new \Exception('param $val2 does not meet required input');
        }

        $this->testDep  = $val1;
        $this->testDep2 = $val2;
    }
}

这是以前在 Atreyu 所基于的 Auryn 版本中不可能实现的;

// try instantiate class 'TestMultiDepsNeeded'
$injector = new Injector;
$injected = $injector->make('Atreyu\Test\TestMultiDepsNeeded', [new TestDependency2()]);

// fails ...

上面的代码不会实例化类 'Atreyu\Test\TestMultiDepsNeeded',因为 Auryn(Atreyu 所基于的 Auryn 版本)将尝试将传递给 $injector->make() 的第二个参数的实体与 TestMultiDepsNeeded::__construct(TestDependency $val1, [...]); 匹配

// try instantiate class 'TestMultiDepsNeeded'
$injector = new Injector;
$injected = $injector->make('Atreyu\Test\TestMultiDepsNeeded', [new TestDependency2()]);

// works now!

在Atreyu 1.0版本中,上述相同的代码将正常运行。现在,Atreyu将查找__construct()方法中的一个特定参数,该参数与Enity类的名称(或其实现的任何接口)相匹配,并将其分配给__construct()方法中的正确参数。

这在您动态实例化具有不同数量参数且参数顺序不断变化的类时尤其有用。

现在还可以实现以下功能;

请注意,TestMultiDepsNeededExample2::__construct()方法没有参数类型提示,请注意其文档块中确实指定了类型提示。

// try instantiate class 'TestMultiDepsNeededExample2'
$injector = new Injector;
$injected = $injector->make('Atreyu\Test\TestMultiDepsNeededExample2', [new TestDependency2()]);

// also works now!

请注意

  • 当方法参数都进行了类型提示且设置了方法文档块时,类型提示的方法参数具有优先权。

示例用例

在PHP社区中,依赖注入容器(DIC)通常被误解。其中一个主要原因是主流应用程序框架中对容器的不当使用。通常,这些框架会将它们的DIC扭曲成服务定位器反模式。这是非常遗憾的,因为一个好的DIC应该是服务定位器的完全相反。

Atreyu NOT 是服务定位器!

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

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

在现实生活中,您不会通过将整个建材商店(希望如此)运送到施工现场来建造房屋,以便访问所需的任何部件。相反,工头(__construct())会要求需要的特定部件(DoorWindow),然后去采购。您的对象应该以相同的方式工作;它们应该只要求完成工作所需的具体依赖项。给House提供整个建材商店的访问权限,至多是不良的OOP风格,最坏的情况是维护噩梦。这里的要点是

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

避免邪恶的单例

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

在上面的代码中,注入器会将字符串定义作为$dsn参数传递给PDO::__construct方法,并在它实例化的任何类需要PDO实例的情况下自动生成共享的PDO实例!

应用程序引导

依赖注入容器(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 Atreyu\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
    }
}

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