vanilla / garden-container

一个依赖注入容器。


README

Build Status Coverage Packagist Version MIT License CLA

花园容器是一个简单但功能强大的依赖注入容器。

特点

  • 通过参数类型提示自动连接依赖项。在您进行任何配置之前,您就可以获得很多功能。
  • 无需使用难以测试的静态变量即可创建对象的共享实例。
  • 可以为基类和接口配置依赖项,并在子类之间共享。
  • 可以为容器中的类配置设置器注入。
  • 可以配置依赖项以引用子容器。使用容器将配置文件中的属性注入到您的应用程序中。
  • 可以更改实现依赖项的类或指定接口的最终类。
  • 可以使用自定义工厂函数来构建对象,以处理重构或边缘情况。

依赖注入的基本原理

考虑以下简单的对象结构,其中控制器对象依赖于模型对象,而该模型对象依赖于数据库连接。

class Controller
{
    public function __construct(Model $model)
    {
    }
}

class Model
{
    public function __construct(PDO $db)
    {
    }
}

为了使用控制器,您需要进行相当多的构建。

$controller = new Controller(new Model(new PDO($dsn, $username, $password);

您可以看到,当您必须创建许多对象或深层对象层次结构时,这会变得多么混乱。使用依赖注入容器,您不必做任何这些。

$dic = new Container();
$controller = $dic->get("Controller"); // dependencies magically wired up

容器检查其构建的对象的类型提示,然后通过递归回容器来构建这些对象。这称为“自动连接”,允许您以非常简单的方式创建任意数量的复杂对象图。如果您稍后想添加更多依赖项,只需向构造函数添加一个参数即可,它将自动解决。

设计良好的应用程序将严重依赖于自动连接,并且只为少数依赖项配置容器。

使用规则配置容器

您可以使用规则覆盖任何类的实例化行为。要为类配置规则,您使用rule()方法选择规则,然后使用任何各种规则获取器和设置器。

命名空间

规则通常使用您将从容器中获取的类的名称命名。如果您使用命名空间,则规则必须使用类的完全限定名命名。名称可以以正斜杠开头,但在处理之前将被删除。

PHP 5.6 引入了 ::class 构造,这是为容器指定类名的一种有用方式。

大小写敏感性

容器应被视为大小写敏感的,但是如果您尝试获取具有不正确大小写的类,则容器可以找到该类,如果该类已经包含或自动加载器是大小写不敏感的。由于大多数PSR自动加载器是大小写敏感的,因此如果容器中大小写不严谨,您可能会遇到错误。

构造函数参数

自动连接仅适用于类型提示参数,但如果一个类有其他参数,您将必须使用setConstructorArgs()方法进行配置。

$dic = new Container();
$dic->rule("PDO")->setConsructorArgs([$dsn, $username, $password]);

在此,新的PDO实例将配置正确的凭据。这种做法的一个好处是,容器仅在从容器检索新对象时才传递配置。

混合类型提示和非类型提示的构造函数参数

如果一个类有一些类型提示和一些常规参数,你只需要用构造函数参数指定非类型提示的参数。其他参数将由容器自动装配。

class Job
{
    public function __construct(Envornment $env, $name, Logger $log)
    {
    }
}

$dic = new Container();
$dic->rule("Job")->setConstructorArgs(["job name"]);

$job = $dic->get("Job");

命名参数

当你向容器的方法传递参数数组时,你可以使用数组键来匹配特定的参数名称。如果你想指定参数列表中的特定参数,这很有用。你也可以通过指定名称来覆盖类型提示的参数。

$dic->rule("Job")->setConstructorArgs([
    "name" => "job name",
    "log" => $dic->get("SysLogger"),
]);

在对象创建时传递构造函数参数

你可以使用 getArgs() 来传递一些或所有构造函数参数。

$dic = new Container();
$pdo = $dic->getArgs("PDO", [$dsn, $username, $password]);

共享对象

将类标记为共享意味着容器在每次请求该类时都会返回相同的实例。这比全局变量或单例要好得多。

$dic = new Container();

$dic->rule("PDO")
    ->setConsructorArgs([$dsn, $username, $password])
    ->setShared(true);

$db1 = $dic->get("PDO");
$db2 = $dic->get("PDO");
// $db1 === $db2

使用调用进行设置注入

你可以在规则中添加方法调用。每个添加的调用在对象首先创建后将按顺序调用。调用与构造函数以类似的方式工作,因此如果存在类型提示参数,它们也会自动装配。

$dic = new Container();

$dic->rule("PDO")
    ->setConsructorArgs([$dsn, $username, $password])
    ->addCall("setAttribute", [PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION])
    ->addCall("setAttribute", [PDO::MYSQL_ATTR_INIT_COMMAND, "set names utf8"]);

指定规则的类

你可以使用 setClass() 方法指定从容器获取项目时创建的类。当你想指定抽象基类或接口的特定子类来满足依赖项时,这很有用。规则也不必代表实际的类;在这种情况下,你必须指定类。

$dic = new Container();

$dic->rule("Psr\Log\LoggerInterface")->setClass("SysLogger");

规则继承

默认情况下,所有子类都会从其基类继承规则。通过这种方式,你可以只为基类定义规则。如果你不想让子类继承规则,则可以通过 setInherit() 覆盖此行为。

class Model {
    ...
}

class UserModel extends Model {
    ...
}

$dic->rule('Model')
    ->setShared(true);

$um1 = $dic->get('UserModel');
$um2 = $dic->get('UserModel');
// $um1 === $um2

接口继承

规则可以以有限的方式从接口继承。如果你在接口上定义规则,任何实现它的类都会调用其方法调用,并且如果它们没有定义自己的,还会使用接口规则构造函数的参数。

$dic->rule("Psr\Log\LogAwareInterface")->addCall("setLogger");

默认规则

有一个默认规则,规则会从中继承。你可以通过选择 defaultRule() 方法或 rule('*') 来修改此规则。

$dic->defaultRule()->setShared(true);
// Now all objects are shared by default.

引用依赖项

你可以指定引用容器内部的参数。为此,你指定参数为 Reference 对象。你使用一个数组来构造引用对象,其中每个项目都是容器或子容器的键。

class Config
{
    public function __construct($path)
    {
        $this->data = json_decode(file_get_contents($path), true);
    }

    public function get($key)
    {
        return $this->data[$key];
    }
}

$dic = new Container();

$dic->rule(Config::class)
    ->setShared(true)
    ->setConstructorArgs(["../config.json"])

    ->rule(PDO::class)
    ->setConstructorArgs([
        new Reference([Config::class, "dsn"]),
        new Reference([Config::class, "user"]),
        new Reference([Config::class, "password"]),
    ]);

$pdo = $dic->get(PDO::class);

在上面的例子中,PDO 对象将使用容器中的 Config 对象提供的信息进行构造。每个引用首先指定 Config::class,因此容器首先查找该对象,然后调用 get() 并传入引用数组中的下一个项目。

ReferenceInterface

Garden\Container 命名空间定义了一个 ReferenceInterface,你可以实现它以使用自定义引用来满足依赖项。还有一个可以用来满足具有可调用参数的引用的 Callback 类。

在容器中设置特定实例

你可以使用 setInstance() 方法将特定对象实例设置到容器中。当你这样做时,该对象始终是共享的。setInstance 的一种用途是将容器放入自身,以便它可以作为依赖项。这被认为是一种反模式,但在某些情况下可能是必要的。

class Dispatcher
    public function __construct(Container $dic) {
        $this->dic = $dic;
    }

    public function dispatch($url) {
        $args = explode('/', $url);
        $controllerName = ucfirst(array_shift($args)).'Controller';
        $method = array_shift($args) ?: 'index';

        $controller = $this->dic->get($controllerName);

        return $this->dic->call([$controller, $method], $args)
    }
}

$dic = new Container();
$dic->setInstance(Container::class, $dic);

$dispatcher = $dic->get(Dispatcher::class);
$dispatcher->dispatch(...);

call() 方法与 call_user_func_array 类似,但它是通过容器调用的,因此依赖项会像其他方法一样自动装配。

别名

你可以指定一个规则是另一个规则的别名。在别名上调用 get() 与调用被别名的规则上的 get() 相同。以下方法用于定义别名。

  • getAliasOf(),setAliasOf()。这些方法将使当前规则别名为另一个规则。注意,别名的规则会忽略其他设置,因为它们是从目标规则中获取的。

  • addAlias(),removeAlias(),getAliases()。这些方法将为当前规则添加别名。这些方法通常更方便,因为您通常希望同时配置规则和设置别名。

为什么使用别名?

当您在基类、类或接口之间存在不一致的类型提示依赖关系时,别名非常有用,并且您希望它们都解析到相同的共享实例。

class Task
{
    public function __construct(LoggerInterface $log)
    {
    }
}

class Item
{
    public function __construct(AbstractLogger $log)
    {
    }
}

$dic = new Container();

$dic->rule(LoggerInterface::class)
    ->setClass("SysLogger")
    ->setShared(true)
    ->addAlias(AbstractLogger::class);

$task = $dic->get(Task::class);
$item = $dic->get(Item::class);
// Both logs will point to the same shared instance.

致谢

本项目深受出色的 DICE 项目以及程度较小的 Aura.Di 项目的启发。Garden Container 中的任何类似于这些项目的代码很可能来自它们,并且仍然是各自所有者的版权。这些项目的开发者比我们聪明得多。