lss/yacontainer

轻量级依赖注入容器

3.0 2022-02-05 00:45 UTC

This package is auto-updated.

Last update: 2024-09-05 06:52:32 UTC


README

PHP 8.1+的最小化依赖注入容器

本项目目标如下

  • 仅按类名存储对象:调用get(MyCleverThing::class)
  • 魔法:最小化配置下的自动加载/自动配置。
  • 极简主义:无需编译,最小化提前声明/配置,尽可能少的代码,仅提供最基本的功能。
  • 性能:去除所有非必要功能。
  • 严格类型:使用phpstan @template,以确保当你调用get(T::class)时,它知道你将接收到类型为T的对象。
  • 适合大型项目或具有许多类但每个请求只使用少数类的单体应用。
  • 100%测试覆盖率

此包(或其早期版本)已经在许多网站上投入生产使用超过十年。无需烦恼。它只需工作。

为什么还需要另一个容器,已经有了这么多非常好的容器?

  • Symfony Dependency Injection需要预先在xml、yml或php中进行声明。可以将构建的容器编译并导出到PHP类中。对于大型项目,生成、编译和导出可能需要几秒钟,并且每次小更改后都需要重新执行。
  • PHP-DI灵活且功能强大,主要维护者支持出色。文档良好。(基准测试)显示其比其他容器慢(尽管基准测试来自2014年)。
  • Auryn简洁简单,具有巧妙的反射缓存和魔法自动加载。它故意不遵循PSR-11。非正式的谷歌搜索表明,从缓存ReflectionClass中几乎没有获得好处
  • Aura.DI需要预先配置每个类。
  • Pimple小巧、优雅,非常适合小型项目。它需要预先声明所有内容。

欢迎提交拉取请求,但请注意上述项目目标。如果你有更复杂的需求,上述提到的其他(编写更好、支持更好、更成熟)的项目将更适合你。

如何使用

创建你的容器并将标量值和别名传递给构造函数,例如

$aliases = [
    MyFoo::class => MyCachedFoo::class,
    MyBarInterface::class => MyBarImplementation::class
];
$container = new Container($_ENV, $aliases);

这是通过静态配置快速设置容器的最快方式。

通过名称请求容器中的任何已自动加载的类,例如 $container->get(My\\Namespace\\MyClass::class);。容器将自动在构造函数中构建依赖关系,以及任何在其构造函数中的递归依赖关系。对于大多数类,这应该不需要进一步的努力。对于需要额外配置的其他类,你可以使用别名、标量注入和工厂方法。

别名

构造函数通常应依赖于接口而不是具体类。那么如何告诉容器使用哪个具体类呢?指定一个别名,如上述示例所示。

标量注入/参数

标量值(int、string、float、bool)可以作为构造函数或设置器参数使用,如果名称与 完全匹配(区分大小写)且未提供默认值。

class DatabaseConnection extends \PDO {
    public function __construct(string $databaseDSN, string $databaseUser, string $databasePassword) 
    {
        $options = ['charset' => 'utf8',PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
        parent::__construct($databaseDSN, $databaseUser, $databasePassword, $options);
    }

    // ... other utility functions    
}

$container = new Container(['databaseDSN' => 'mysql:host=localhost;dbname=theDBName', 'databaseUser' => 'theUserName', 'databasePassword' => 'thePassword']);
$databaseConnection = $container->get(DatabaseConnection::class);

如果需要,您还可以使用 $container->addScalar() 来添加更多。

如果为构造函数参数提供了默认值,它将被尊重,标量值将不会注入。如果您需要传递不同的值,请使用工厂类。

您还可以使用可调用的标量值。可调用函数只会被调用一次,然后将其值替换为所有后续使用。

$container->addScalar('maximumPassengers', function (Configuration $config) {
    return $config->getMaximumPassengers();
});

工厂方法和可调用对象

对于构建复杂或需要许多其他类不需要的类的类,请使用工厂。

$fuelPercent = 75;
$container = new Container();
$container->addFactory(Car::class, function (EngineInterface $engine) use ($fuelPercent): Car {
    $result = new Car($engine);
    $result->refuel($fuelPercent);
    return $result;
});

设置器注入

设置器注入可以通过工厂方法来模拟。调用设置器后

共享实例

默认情况下,所有生成的对象都是共享的。这意味着对于同一个类名,每次调用 get() 都将返回相同的类实例。如果您需要每次都返回不同的实例,请提供一个函数来告诉容器要共享哪些实例。

$container->setShouldShare(function (string $className): bool { return $className !== Car::class; });

将为每个 get() 调用构建一个新的 Car 类实例。

要禁用共享(每次都创建新实例)

$container->setShouldShare(function (string $className): bool { return false; });

忘记

如果您大多数时候需要共享实例,但出于某些特殊原因偶尔需要新的实例,请使用 forget() 来忘记当前的实例。下一次调用 get() 将创建一个新的实例。

$car = $container->get(Car::class);
$container->forget(Car::class);
$aDifferentCar = $container->get(Car::class);

PSR-11 容器

我们故意不实现 Psr\Container\ContainerInterface,因为

  • PSR-11 不够严格。它是一个通用的字典,设计用来存储任何由任何字符串键入的混合事物
  • 许多包使用 v1.0 或 v2.0,跨依赖管理变得复杂。减少一个依赖项是有帮助的。
  • 在 PSR-11 代理中包装此容器是微不足道的:请参见下面
class Psr11ContainerException extends \InvalidArgumentException implements Psr\Container\ContainerExceptionInterface {}

class Psr11Container implements Psr\Container\ContainerInterface {
    public __construct(private LSS\Container $wrapped) {}
    
    public function get(string $id) {
        try {
            return $this->wrapped->get($id);
        } catch (\Throwable $ex) {
            throw new Psr11ContainerException('Cannot build ' . $id, 0, $ex);
        }
    }
    
    public function has(string $id): bool 
    {
        return $this->wrapped->has($id);
    }
}

这**不是一个服务定位器

避免将容器作为依赖项传递给您创建的类的诱惑。使用此方法最好的方式是在您的引导代码中构建您的应用程序的部分,使其成为一个单一单元。 Auryn 有一个很好的例子