mindplay/unbox

快速、简单、易于使用的DI容器

3.1.1 2024-05-06 08:31 UTC

README

Unbox

PHP Version PHPStan Build Status Code Coverage

Unbox 是一个快速、简单、具有强烈观点的依赖注入容器,具有平缓的学习曲线。

兼容 PSR-11

要从旧版本(3.x 之前)升级,请参阅升级指南

安装

使用 Composer: require mindplay/unbox

介绍

这个库实现了一个具有非常小的内存占用、少数概念和合理的学习曲线、良好性能以及通过主要使用闭包进行 IDE 支持的快速和简单配置的依赖注入容器。

该容器能够解析构造函数参数,通常自动进行,只需要类名即可配置。它还可以解析任何可调用的参数,包括实现了 __invoke() 的对象。它还可以用作通用工厂类,能够创建任何可以解析构造函数参数的对象 - 这在您自己的工厂类中是一个常见的用例,例如控制器工厂或操作分发器。

快速概览

下面,您可以找到完整的指南和完整文档 - 但为了给您一个关于这个库做什么的概念,让我们从一段快速代码示例开始。

对于这个基本示例,我们假设您有以下相关类型

interface CacheInterface {
    // ...
}

class FileCache implements CacheInterface {
    public function __construct($path) { ... }
}

class UserRepository {
    public function __construct(CacheInterface $cache) { ... }
}

Unbox 有两个阶段的生命周期。第一阶段是创建一个 ContainerFactory - 这个类提供了引导和配置功能。第二阶段从调用 ContainerFactory::createFactory() 开始,该调用创建实际的 Container 实例,该实例提供了允许客户端代码调用函数和构造函数等功能的设施。

让我们使用那些依赖关系引导一个 ContainerFactory,在某个地方的 "引导" 文件中

use mindplay\unbox\ContainerFactory;

$factory = new ContainerFactory();

// register a component named "cache":
$factory->register("cache", function ($cache_path) {
    return new FileCache($cache_path);
});

// register "CacheInterface" as a component referencing "cache":
$factory->alias(CacheInterface::class, "cache");

// register "UserRepository" as a component:
$factory->register(UserRepository::class);

然后配置 $cache_path 以用于 cache 组件,将其添加到某个地方的 "配置" 文件中

$factory->set("cache_path", "/tmp/cache");

现在 ContainerFactory 已完全引导,我们准备好创建一个 Container

$container = $factory->createContainer();

在这个简单示例中,我们现在已经完成了 ContainerFactory,它可以简单地超出作用域。(在更复杂的场景中,如长期运行的 ReactPHP-PM 应用程序中,您可能想保持对 ContainerFactory 的引用,以便为每个请求创建一个新的 Container。)

现在,您可以从 Container 中提取 UserRepository,要么直接请求它

$users = $container->get(UserRepository::class);

或者,使用带类型提示的闭包来支持 IDE

$container->call(function (UserRepository $users) {
    $users->...
});

为了完成这个快速示例,假设您有一个控制器

class UserController
{
    public function __construct(UserRepository $users)
    {
        // ...
    }

    public function show($user_id, ViewEngine $view, FormHelper $form, ...)
    {
        // ...
    }
}

使用容器作为工厂,您可以创建任何控制器类的实例

$controller = $container->create(UserController::class);

最后,您可以通过依赖注入来分发 show() 动作 - 作为简单的示例,我们将直接将 $_GET 作为参数注入到方法中

$container->call([$controller, "show"], $_GET);

使用 $_GET 作为调用参数,UserController:show() 中的 $user_id 参数将被解析为 $_GET['user_id']

这就是快速的高级概述。

API

如果您已经熟悉依赖注入,只想了解API的样子,以下是对 ContainerFactory API 的快速概述。

register(string $type)                                 # register a component (for auto-creation)
register(string $type, array $map)                     # ... with custom constructor arguments
register(string $name, string $type)                   # ... with a specific name for auto-creation
register(string $name, string $type, array $map)       # ... and custom constructor arguments
register(string $name, callable $func)                 # ... with a custom creation function
register(string $name, callable $func, array $map)     # ... and custom arguments to that closure

set(string $name, mixed $value)                        # directly insert an existing component

add(ProviderInterface $provider)                       # register a configuration provider

alias(string $new_name, string $ref_name)              # make $ref_name available as $new_name

configure(callable $func)                              # manipulate a component upon creation
configure(callable $func, array $map)                  # ... with custom arguments to the closure
configure(string $name, callable $func)                # ... for a component with a specific name
configure(string $name, callable $func, array $map)    # ... with custom arguments

ref(string $name) : BoxedValueInterface                # create a boxed reference to a component

registerFallback(ContainerInterface $container)        # register a fallack container

requires(string $requirement, string $description)     # defines a Requirement
provides(string $requirement, string $description)     # fulfills an abstract Requirement

createContainer() : Container                          # create a bootstrapped Container instance

以下是对 Container API 的快速概述。

get(string $name) : mixed                              # unbox a component
has(string $name) : bool                               # check if a component is defined/exists
isActive(string $name) : bool                          # check if a component has been unboxed

call(callable $func) : mixed                           # call any callable an inject arguments
call(callable $func, array $map) : mixed               # ... and override or add missing params

create(string $class_name) : mixed                     # invoke a constructor and auto-inject
create(string $class_name, array $map) : mixed         # ... and override or add missing params

如果您对依赖注入不太熟悉,或者这些内容让您感到困惑,请不要慌张——以下指南涵盖了所有内容。

术语

以下文档中使用了以下术语:

  • Callable:指的是PHP手册中定义的 callable 伪类型 定义

  • Component:容器中注册的任何对象或值,无论通过类名、接口名还是其他任意名称注册。

  • Singleton:当我们说“singleton”时,意味着在同一个容器实例中只有一个具有给定名称的组件;当然,您可以拥有多个容器实例,因此每个组件仅在同一个容器中是“singleton”。

  • Dependency:在我们的上下文中,这意味着任何被另一个组件、构造函数(当使用容器作为工厂时)或任何可调用项所要求的已注册组件。

依赖解析

任何参数,无论是手动调用的闭包,还是解析更长的依赖链时自动调用的构造函数,都是根据一致的一组规则进行解析——按优先级排序。

  1. 如果您自己提供参数,例如在注册组件(或配置函数)或调用可调用项时,这始终具有优先权。参数可以包括包装值,例如(通常是)对其他组件的引用,这些值将尽可能晚地解包。

  2. 类型提示是解析单例的首选方式,例如您在同一个容器中只有一个实例(或一个“首选”实例)的类型。单例通常以其类名、接口名或有时同时注册。

  3. 参数名称,例如与精确参数名称(不带 $)匹配的组件——这仅在安全的情况下有效,在大多数情况下都是安全的,唯一的例外是使用 create() 调用的构造函数,其中容器中的组件名称恰好与构造函数中的参数名称匹配。(通过 $map 参数给出的构造函数参数当然是安全的。)

  4. 如果提供了默认参数值,则将其用作最后的手段——这在例如 function ($db_port = 3306) { ... } 的情况下很有用,它允许对简单值进行可选配置,并具有默认值。

对于使用类型提示解析的依赖项,忽略参数名称——反之亦然:如果依赖项是通过参数名称解析的,则忽略类型提示,但当然,PHP在调用函数/方法/构造函数时会检查它。请注意,以任何方式使用类型提示都是良好的实践(当可能时),因为这提供了具有IDE支持的自我文档化的配置。

指南

在以下章节中,我们假设一个 ContainerFactory 实例在作用域内,例如:

use mindplay\unbox\ContainerFactory;

$factory = new ContainerFactory();

引导

引导容器最常用的方法是 register() —— 这是您注册组件进行依赖注入的方法。

此方法通常采用以下形式之一

register(string $type)                                 # register a component (for auto-creation)
register(string $type, array $map)                     # ... with custom constructor arguments
register(string $name, string $type)                   # ... with a specific name for auto-creation
register(string $name, string $type, array $map)       # ... and custom constructor arguments
register(string $name, callable $func)                 # ... with a custom creation function
register(string $name, callable $func, array $map)     # ... and custom arguments to that closure

其中:

  • $name 是组件名称
  • $type 是完全限定的类名
  • $map 是参数的混合列表/映射(见下文)
  • $func 是自定义工厂函数

当使用 $type 而不使用 $name 时,组件名称假定也是正在注册的类型名称。

$map 参数是参数的混合列表和/或映射。也就是说,如果您包含没有键的参数(如 ['apple', 'pear']),则这些被视为位置参数,而具有键的参数(如 ['lives' => 9])将与被调用或构造函数的参数名称匹配。

当通过$map提供自定义参数时,通常使用$factory->ref('name')来获取组件的“包装”引用——当注册的组件首次创建时,任何“包装”参数都会在那个时刻“解包”。换句话说,这允许你以“懒加载”的方式提供其他组件作为参数,直到实际需要时才激活它们。

如果提供了可调用对象$func,则将其注册为您的自定义组件创建函数——对于这个闭包进行依赖注入,所以如果你关心IDE支持,这通常是指定组件创建方式的最佳方式。(你应该!)

示例

以下示例都是上述形式的有效用例

  • register(Foo::class)通过类名注册组件,并将尝试自动解析其构造函数的所有参数。

  • register(Foo::class, ['bar'])通过类名注册组件,并将使用'bar'作为第一个构造函数参数,并尝试解析其他参数。

  • register(Foo::class, [$factory->ref(Bar::class)])创建一个对已注册组件Bar的“包装”引用,并将其作为第一个参数提供。

  • register(Foo::class, ['bat' => 'zap'])通过类名注册组件,并将使用'zap'作为构造函数参数$bat的值,并尝试解析任何其他参数。

  • register(Bar::class, Foo::class)在另一个名称Bar下注册组件Foo,这可能是接口或抽象类。

  • register(Bar::class, Foo::class, ['bar'])与上面相同,但使用'bar'作为第一个参数。

  • register(Bar::class, Foo::class, ['bat' => 'zap'])与上面相同,但,猜猜。

  • register(Bar::class, function (Foo $foo) { return new Bar(...); })使用自定义工厂函数注册组件。

  • register(Bar::class, function ($name) { ... }, [$factory->ref('db.name')]);注册一个带有对组件“db.name”引用的组件创建函数作为第一个参数。

实际上,你可以将$func视为一个可选参数。

提供的参数值可以包括任何BoxedValueInterface,如(常见)由ContainerFactory::ref()创建的“包装”组件引用——这些将在尽可能晚的时候“解包”。

别名

有时你需要以两个不同的名称注册相同的组件——一个常见的用例是同时为具体类型和抽象类型注册相同的组件,例如,为一个类和一个接口。

例如,注册缓存组件两次是很常见的

$factory->register(CacheInterface::class, function () {
    return new FileCache();
});

$factory->alias("db.cache", CacheInterface::class); // "db.cache" becomes an alias!

$container = $factory->createContainer();

var_dump($container->get("db.cache") === $container->get(CacheInterface::class)); // => bool(true)

在这个示例中,使用别名意味着默认情况下"db.cache"将解析为CacheInterface,但同时也提供了以不同的实现来覆盖"db.cache"定义的能力,而不会影响可能也在使用CacheInterface作为默认的其他组件。

直接插入

并非所有依赖项都昂贵到需要创建——简单值(如主机名和端口号)不会从使用register()的延迟初始化中受益,而应直接插入容器中。

$factory->set("db.host", "localhost");
$factory->set("db.port", "12345");

set()的另一个常见用例是注入无法延迟创建的对象。

覆盖

要覆盖现有组件,只需使用已注册的组件名称调用register()——这将完全替换现有的组件定义。

请注意,覆盖组件不会影响任何已注册的配置函数——因此,如果你要覆盖组件,新的组件必须与被替换的组件兼容。配置将在下面介绍。

配置

要执行已注册组件的附加配置,请使用configure()方法。

此方法有以下形式之一

configure(callable $func)
configure(callable $func, array $map)
configure(string $name, callable $func)
configure(string $name, callable $func, array $map)

其中:

  • $name 是正在配置的组件的名称
  • $func 是以某种方式配置组件的函数
  • $map 是参数的混合列表/映射(如上所述)

可调用的 $func 将通过依赖注入来调用 - 该函数的第一个参数是正在配置的组件;虽然您并非严格要求这样做,但您应该为它进行类型提示(如果可能,为了IDE支持)。任何额外的参数也将得到解决。

可选的数组 $map 是参数的混合列表/映射,如上所述。

如果没有提供 $name,则将从给定的 $func 的第一个参数中推断组件名称。

例如,假设您已配置了一个 PDO 组件

$factory->register(PDO::class, function ($db_host, $db_name, $db_user, $db_password) {
    $connection = "mysql:host={$db_host};dbname={$db_name}";

    return new PDO($connection, $db_user, $db_password);
});

在配置文件中,简单的值(如 $db_host)可以直接插入,例如使用 $factory->set("db_host", "localhost") - 但是假设您需要在连接创建后执行某些操作?这就是 configure() 发挥作用的地方

$factory->configure(function (PDO $db) {
    $db->exec("SET NAMES utf8");
});

请注意,在这个例子中,configure() 将从类型提示中推断组件名称 "PDO" - 在有多个命名 PDO 实例的情景中,您必须显式指定组件名称作为第一个参数,例如

$factory->configure("logger.pdo", function (PDO $db) {
    $db->exec("SET NAMES utf8");
});
属性或 Setter 注入

此库不支持属性或 setter 注入,但可以通过在 configure() 调用中执行这些操作来实现 - 例如

$factory->configure(function (Connection $db, LoggerInterface $logger) {
    $db->setLogger($logger);
});

在这个例子中,当首次使用 Connection 时,依赖项 LoggerInterface 将被解包并通过 setter 注入。我们相信这种方法比提供一个接受方法名作为参数的函数更安全 - 闭包更强大、更安全,并提供完整的 IDE 支持、检查、自动化重构等。

修改

您可以使用 configure() 来修改容器中的值(如字符串、数字或数组)。

例如,假设您已定义了一个中间件堆栈数组

$factory->set("app.middleware", function () {
    return [new RouterMiddleware, new NotFoundMiddleware];
);

如果您需要向堆栈中添加项,您可以这样做

$factory->configure("app.middleware", function ($middleware) {
    $middleware[] = new CacheMiddleware();

    return $middleware;
});

请注意 return 语句 - 这正是导致值在容器中更新的原因。

装饰

装饰器 模式是另一种可以使用 configure() 实现的模式 - 例如,假设您使用产品存储库实现和接口启动了容器

$factory->register(ProductRepository::class, function () { ... });

$factory->alias(ProductRepositoryInterface::class, ProductRepository::class);

现在假设您实现了一个缓存的 产品存储库装饰器 - 您可以通过创建并返回装饰器实例来启动它

$factory->configure(function (ProductRepositoryInterface $repo) {
    return new CachedProductRepository($repo);
});

请注意,以这种方式替换组件时,当然您必须确保替换具有可以传递给接收者构造函数或方法的类型,以便通过类型检查。

打包提供者

您可以通过实现 ProviderInterface 将一组 register()configure() 调用打包以方便重用 - 例如

class MyProvider implements ProviderInterface
{
    public function register(ContainerFactory $factory)
    {
        $factory->register(...);
        $factory->configure(...);
        // ...
    }
}

然后您可以使用提供者轻松启动您的项目,例如

$factory->add(new MyProvider);
$factory->add(new TestDependenciesProvider);
$factory->add(new DevelopmentDebugProvider);
// ...

当然,提供者也可以调用 ContainerFactory::add() 来启动其他提供者 - 考虑到这一点,您可以使例如开发或生产设置变得像调用例如 $container->add(new DevelopmentProvider) 一样简单,以提供快速开发设置的全部启动。即使有人想要覆盖例如默认开发设置中的某些注册,他们当然仍然可以这样做,例如通过再次调用 register() 来覆盖组件,如需。

提供者要求

在大型、模块化的架构中,您可能有许多具有相互依赖关系的提供者,这可能会在规模扩大时变得难以管理。

由于提供者存在于容器之外,因此可以使用需求的概念来定义可验证的提供者依赖关系,这些依赖关系将在调用createContainer()时进行检查。

可以通过调用requires()来定义需求,并且多个提供者可能指定相同的需求——可能是出于不同的原因,这些原因可以使用可选的$description参数进行描述。

可以通过调用register()provides()来满足需求。

组件需求

提供者可能依赖于消费者手动注册一个组件。

例如,以下提供者要求你注册一个PDO连接实例

class MyProvider implements ProviderInterface
{
    public function register(ContainerFactory $factory)
    {
        $factory->requires(PDO::class, "a PDO instance connected to a MySQL database");
        
        // ...
    }
}

如果没有手动注册PDO实例就尝试启动此提供者,一旦调用createContainer()就会抛出异常,这比其他情况下可能会遇到的NotFoundException更容易调试,并且可能直到尝试解析实际依赖于数据库连接的组件时才发生。

抽象需求

提供者可能有抽象需求——这是不能用简单的组件依赖来表达的。

例如,以下提供者要求你只是表明你已经启动了一个“支付网关”——无论这对特定提供者来说意味着什么

class MyProvider implements ProviderInterface
{
    public function register(ContainerFactory $factory)
    {
        $factory->requires("acme.payment-gateway", "please refer to acme's documentation");
        
        // ...
    }
}

另一个提供者需要明确指出满足了这个抽象需求

class MyPaymentProvider implements ProviderInterface
{
    public function register(ContainerFactory $factory)
    {
        $factory->provides("acme.payment-gateway");
        
        // ...
    }
}

请注意,抽象需求应该是最后的手段——组件依赖通常更简单,更容易理解。这个功能主要存在是为了支持复杂、大规模的模块化框架。

后备容器

你可以使用这个功能来构建具有不同组件生命周期的分层架构。

请注意,这种类型的架构更多地关于分离依赖关系到架构层,而不是关于重用(在大多数情况下,可以通过仅重用提供者来实现重用)。

此功能最常见的用例是在长时间运行的“守护进程”中,例如Web主机,其中可以使用此功能将短生命周期的、请求特定的组件与长时间运行的服务分离。例如,控制器或会话模型可能被注册在每次请求创建和销毁的容器中——而数据库连接或SMTP客户端可能被注册在一个长时间运行的单一后备容器中,这样应用程序运行期间就不需要重复启动开销。

这种分离在架构方面也很有用,因为它迫使你故意地、有意识地关注对请求特定组件的依赖,因为这些组件在长时间运行的容器中不可用。同样,也许你的项目还有一个基于控制台的客户端,可以使用这种类型的架构来确保命令行依赖关系不会提供给Web主机的组件——等等。

在实际操作中,要注册后备容器,请在ContainerFactory实例上使用registerFallback方法。具有一个或多个已注册后备的工厂创建的容器将内部查询后备(按照它们添加的顺序)以查找任何尚未在容器本身中注册的组件——这意味着对hasget的调用将传播到任何已注册的后备容器。

典型的方法是将短期服务的容器工厂作为组件注册在长期运行的主要服务容器中——例如

$app_factory = new ContainerFactory();

// components we can reuse across many requests:

$app_factory->register(DatabaseConnection::class);

// factory for containers for individual requests:

$app_factory->register("request-context", function (ContainerInterface $app_container) {
    $request_container_factory = new ContainerFactory();

    // enable request-specific containers to look up long-lived services in the main container:

    $request_container_factory->registerFallback($app_container);

    return $request_container_factory;
});

// we can now register short-lived components against the `request-context` container factory:

$app_factory->configure("request-context", function (ContainerFactory $request_container_factory) {
    $request_container_factory->register(LoginController::class); // depends on DatabaseConnection
});

// now create the long-lived app container, e.g. in your "index.php" or CLI daemon script:

$app_container = $app_factory->createContainer();

有了这种启动设置,你现在可以根据需要创建request-context容器的实例,例如在一个长期运行的组件中处理传入的Web请求

$request_container = $app_container->get("request-context")->createContainer();

$controller = $request_container->get(LoginController::class);

$request_container超出作用域时,任何短生命周期的组件,如LoginController,将与容器一起释放——而任何长期运行的组件,如DatabaseConnection,将保留在$app_container中,相同的实例将被传递给每个新的控制器实例。

使用容器

通过简单地从容器中提取组件来获取容器内容似乎非常方便,因此很有吸引力——但通常是不正确的!您应该了解差异并避免将容器用作服务定位器

经验法则

永远不要使用容器查找组件的直接依赖项。

相反,使用容器代表其他组件查找依赖项通常是可行的。

在以下部分,我们将假设有一个Container实例在作用域中,例如。

$factory = new ContainerFactory();

// ... bootstrapping ...

$container = $factory->createContainer();

组件访问的最基本形式是直接查找

$cache = $container->get(CacheInterface::class);
$db_name = $container->get("db_name");

更间接的组件访问形式是通过解析参数进行间接查找

$container->call(function (CacheInterface $cache, $db_name) {
    // ...
});

这两个示例的结果是相同的——但重要的是要注意,在call()示例中,两个参数以两种不同的方式解析:CacheInterface参数通过类名解析,而$db_name参数是通过参数名解析。

后者之所以可行,仅仅是因为$db_name组件注册了那个精确的名称——如果它注册了例如"db.name"这样的名称,容器将无法自动解析此参数;相反,您必须编写

$container->call(function (CacheInterface $cache, $name) {
    // ...
}, ["name" => $container->ref("db.name")]);

请注意,call()将接受任何类型的调用

工厂功能

可以使用create()方法调用构造函数,按需创建任何类的实例。

理解一个重要的事情是,例如register()configure()对这个功能没有影响——这个方法的目的是创建容器中没有注册为组件的类型实例,但(可能)有依赖关系可以由容器提供。

控制器是一个很好的例子——你很可能不想将每个单独的控制器类作为组件注册到容器中;相反,你可能需要一个控制器工厂,能够创建任何控制器。

以下是一个简单的控制器工厂实现示例,该工厂解析典型的"foo/bar"路由字符串,例如FooController::bar()——如下所示

class Action
{
    public function __construct(Controller $controller, $action, array $params) { ... }
}

class ControllerFactory
{
    /** @var FactoryInterface */
    private $factory;

    public function __construct(FactoryInterface $factory) { ... }

    public function create($route, array $params)
    {
        list($controller_name, $action_name) = explode("/", $route);

        $controller_class = ucfirst($controller_name) . "Controller";

        $controller = $this->factory->create($controller_class);

        return new Action($controller, $action_name, $params);
    }
}

注意构造函数中的FactoryInterface类型提示——在您只关心将容器用作工厂的情况下,您应该对此特性进行类型提示。

检查

您可以使用has()isActive()检查容器中组件的状态。

要检查组件是否已定义,请使用has()——例如

var_dump($container->has("foo")); // => bool(false)

$container->set("foo", "bar");

var_dump($container->has("foo")); // => bool(true)

无论组件是通过set()直接插入,还是通过register()定义,has()方法都将返回true

要检查组件是否已激活,请使用isActive()——例如

$container->register("foo", function () { return "bar"; });

var_dump($container->isActive("foo")); // => bool(false)

$foo = $container->get("foo"); // component activates on first use

var_dump($container->isActive("foo")); // => bool(true)

当组件第一次使用时,它被认为是“激活”的——组件可能通过调用get()直接激活,也可能通过依赖关系的级联激活间接激活。

有见地的

少即是多。我们只支持创建美观架构真正必需的功能——我们不提供大量的“便利”功能来支持我们不使用或不太常见的模式,这些模式可以很容易地使用我们提供的功能实现。

功能

  • 以生产力为导向 - 优先使用大量的closures以实现IDE支持:重构友好定义,具有自动完成支持、检查等。

  • 以性能为导向,仅当它不阻碍API时。

  • 多才多艺 - 使用少量公共方法支持许多不同的注册和配置选项,包括值修改、装饰器等。

  • 零配置 - 我们不包含任何可选特性或可配置行为:容器始终表现出一致的行为,具有相同的可预测性能和互操作性。

  • PHP 5.5+ 支持 ::class,而且你真的不应该使用更旧的版本。

非特性

  • 无注解 - 因为在你的领域模型中分散容器配置是极其糟糕的想法。

  • 无自动装配 - 因为 $container->register(Foo::name) 并不麻烦,并且明确地将某物指定为服务;意外地将非单例视为单例可能是一种奇怪的经历。

  • 无缓存 - 因为配置容器真的不应该有如此多的开销,以至于需要缓存。Unbox 非常快。

  • 无属性/设置器注入 因为它模糊了你的依赖关系 - 使用构造函数注入,对于可选依赖项,使用可选构造函数参数;毕竟,你不再需要计算参数数量,因为所有内容都将被注入。(如果你有很好的理由通过属性或设置器注入某些内容,你可以在 configure() 调用内部的闭包中这样做,并完全支持 IDE。)

  • 无语法 - 我们绝对不发明或解析任何特殊的字符串语法。任何可以用自定义语法解决的问题,也可以用干净、简单的 PHP 代码解决。

  • 无链式 API,因为链式调用(在 PHP 中)与源代码控制不太友好。

  • 所有注册的组件都是单例 - 我们不支持工厂注册;如果你需要注册工厂,正确的做法是实现一个实际的工厂类(这通常是长期来看更好的选择),或者将工厂闭包本身注册为命名组件。

基准测试

这并不是作为一个竞争基准,而是为了让你了解从三个具有非常不同目标、不同质量和从最小、最简单到最大、最雄心勃勃的三个非常不同的 DI 容器中进行选择时的性能影响。

  • pimple 是最简单的 DI 容器,没有任何花哨的功能,学习曲线几乎为零,总共有约 250 行代码

  • unbox 只有几个类和接口 - 比 pimple 有更多概念和稍微陡峭的学习曲线,总共有约 350 行代码

  • php-di 是一个功能丰富的依赖注入框架,拥有所有花哨的功能 - 功能丰富,但概念和学习曲线更多,开销更大,总共有约 3000 行代码

包含的 简单基准测试 在 Windows 11 的 WSL2 下,使用 PHP 8.2.10 生成以下基准测试结果。

配置容器的耗时

unbox: configuration ............................. 0.006 msec ....... 41.78% ......... 1.00x
pimple: configuration ............................ 0.008 msec ....... 51.25% ......... 1.23x
php-di: configuration ............................ 0.013 msec ....... 89.02% ......... 2.13x
php-di: configuration [compiled] ................. 0.015 msec ...... 100.00% ......... 2.39x

在容器中解析依赖关系的耗时,首次访问时

pimple: 1 repeated resolutions ................... 0.002 msec ........ 9.14% ......... 1.00x
php-di: 1 repeated resolutions [compiled] ........ 0.004 msec ....... 18.88% ......... 2.06x
unbox: 1 repeated resolutions .................... 0.006 msec ....... 26.47% ......... 2.90x
php-di: 1 repeated resolutions ................... 0.022 msec ...... 100.00% ........ 10.94x

多次后续查找的耗时

pimple: 3 repeated resolutions ................... 0.003 msec ....... 11.44% ......... 1.00x
php-di: 3 repeated resolutions [compiled] ........ 0.004 msec ....... 18.55% ......... 1.62x
unbox: 3 repeated resolutions .................... 0.006 msec ....... 27.24% ......... 2.38x
php-di: 3 repeated resolutions ................... 0.023 msec ...... 100.00% ......... 8.74x

pimple: 5 repeated resolutions ................... 0.003 msec ....... 13.36% ......... 1.00x
php-di: 5 repeated resolutions [compiled] ........ 0.005 msec ....... 19.68% ......... 1.47x
unbox: 5 repeated resolutions .................... 0.007 msec ....... 28.02% ......... 2.10x
php-di: 5 repeated resolutions ................... 0.023 msec ...... 100.00% ......... 7.48x

pimple: 10 repeated resolutions .................. 0.004 msec ....... 17.48% ......... 1.00x
php-di: 10 repeated resolutions [compiled] ....... 0.005 msec ....... 22.15% ......... 1.27x
unbox: 10 repeated resolutions ................... 0.007 msec ....... 29.83% ......... 1.71x
php-di: 10 repeated resolutions .................. 0.024 msec ...... 100.00% ......... 5.72x