firehed/container

具有高级自动装配和编译支持的依赖倒置容器。符合PSR-11规范。

0.9.1 2024-08-22 20:43 UTC

README

符合PSR-11规范的依赖倒置容器

Test Static analysis Lint Packagist

为什么还需要另一种容器实现方式?

创建此容器的首要动机是为了拥有一种针对长时间运行过程(如ReactPHP和PHP-PM)容器化部署优化的容器实现。

用法和API高度受PHP-DI启发,但增加了在定义时支持工厂的功能(而不是仅通过make在访问时)。这是为了减少在并发环境中的服务不可预测行为,同时严格遵循PSR容器规范。

与PHP-DI的不同之处

  • 容器上仅存在get()has()方法

  • factory函数的行为完全不同:它包装的闭包每次通过get请求时都会被调用(PHP-DI将其暴露为$container->make())。在PHP-DI中,factory只是定义服务的闭包的另一种语法。

  • 当一个接口映射到一个实现时,默认行为是返回配置的实现。在PHP-DI中,SomeInterface::class => autowire(SomeImplementation::class)不会指向显式配置的SomeImplementation

  • 添加了接口到实现的简写语法

  • 在递归解析依赖时,所有必需的参数也必须显式配置(参见上述内容)

  • 不支持注解。在未来版本中,@param注解可能被支持;@Inject永远不会被支持。

设计观点

像许多自动装配DI容器一样,这个容器也有一些有见地的设计决策。你可能同意也可能不同意,但重要的是要记录下来,以帮助你做出明智的选择,判断这个库是否适合你。

  • 这基于为你的应用程序部署过程有一个独特的构建/编译阶段。在生产就绪的编译容器中,不会发生隐式自动装配,这会产生性能提升。将自动装配的类名添加到任何定义文件中以显式连接它(Foo::class,就足够了,见下文“自动装配”)。

  • 任何有效的类字符串的$id都应该返回该类的实例(或接口、枚举)。从0.6版开始,这反映在提供的数据类型信息中:当检测到class-string时,->get($id)有PHPStan的泛型信息。这(目前)不是在运行时强制执行的,但请注意,例如$container->get(LoggerInterface::class);将被指示为LoggerInteface到静态分析工具,如果定义没有这样做,你可能会遇到冲突。

  • 所有文件都应该始终包含。不要根据环境跳过文件。相反,定义应该是条件性的。示例

    <?php
    
    return [
        MyInterface::class => function ($c) {
            return $c->get('use_mocks')
                ? $c->get(MyImplementationMock::class)
                : $c->get(MyImplementationReal::class);
        },
    ];

安装

composer require firehed/container

用法

到容器的接口主要是通过BuilderInterface

有两种实现

构建器

Builder类将创建一个开发容器,该容器会动态确定依赖关系。这主要用于开发 - 在请求期间执行自动装配类的反射,这在更改时很方便,但会增加开销。

编译器

Compiler类将生成优化代码并将其写入文件一次,然后加载该文件,并返回一个使用优化代码的容器。仅在实际编译时执行自动装配的反射,因此它将比开发容器运行得更快。然而,每当任何定义发生变化(包括自动装配类的构造函数签名)时,必须重新编译文件。

提示

强烈建议在非开发环境中使用 Compiler 实现,并在构建过程中编译容器。

运行编译器

第一次调用 build() 时,编译过程会自动运行。

示例

<?php
declare(strict_types=1);

// Include Composer's autoloader if not already done
require 'vendor/autoload.php';

// If using a tool like dotenv, apply it here
/*
if (file_exists(__DIR__.'/.env')) {
    Dotenv\Dotenv::create(__DIR__)->load();
}
 */

$isDevMode = getenv('ENVIRONMENT') === 'development';

if ($isDevMode) {
    $builder = new Firehed\Container\Builder();
} else {
    $builder = new Firehed\Container\Compiler();
}

// Each definition file must return a definition array (see below)
foreach (glob('config/*.php') as $definitionFile) {
    $builder->addFile($definitionFile);
}

return $builder->build();

提示

如果您遵循上述模式(配置文件在一个目录中),AutoDetect 类可以为您完成这项工作。请参阅下文的自动检测

定义API

添加到 BuilderInterface 的所有文件必须 返回 一个数组。数组的键将映射到可以与 has($id) 检查其存在的 $id,而数组的值将在提供这些键给 get($id) 时返回。

强烈建议类实例使用它们的完全限定类名作为数组键,并另外创建一个接口到实现的映射。当键是一个 interface 的完全限定名称,而值是一个映射到类名的字符串时,这会自动发生。后者将在键为完全限定的 interface 名称且值为类名字符串时自动发生。

注意

库输出实现了一个 TypedContainerInterface,它为 PSR-11 添加了可由 PHPStan 和 Psalm 等工具读取的 docblock 泛型。它假定您正在遵循上述约定;如果不这样做,可能会导致误导性的输出。这不会在运行时产生影响,只会在开发和 CI 时有所帮助。

示例

最简洁的示例都是单元测试的一部分:tests/ValidDefinitions

简单值

如果提供一个标量、数组或 Enum 作为值,则该值将被返回,不进行修改。

异常:如果值是一个 string 并且键是一个已声明的 interface 的名称,它将自动被视为接口到实现的映射,并按 InterfaceName::class => autowire(ImplementationName::class) 处理。这样做时,您 应该 使用 ::class 文字面量编写映射;例如,\Psr\Log\LoggerInterface::class => SomeLoggerImplementation::class。与字符串相比,这种方法不仅提供了在阅读文件时的额外清晰性,还允许静态分析工具检测一些错误。

对象不能直接作为值提供,而必须作为闭包提供;请参见下文。这是因为编译器无法创建实际的对象实例,因此它只能在开发模式下工作。

闭包

如果提供一个闭包作为值,那么当调用 get() 时,该闭包将被执行,并返回它返回的值。容器将被提供为闭包的第一个和唯一参数,因此定义可能依赖于其他服务。对于没有依赖项的服务,闭包可以定义为不接受任何参数的函数(一个“thunk”),尽管在执行时容器仍然会被传递(并且随后被忽略)。不要使用 use() 语法来访问其他容器定义。

由于计算值被缓存(除了用 factory 包装的情况,请参见下文),闭包只会在第一次调用 get() 时执行。

任何应该返回对象实例的值定义都必须由闭包定义,或者使用以下描述的帮助程序之一。在定义文件中直接实例化类是无效的。

<?php
use Psr\Container\ContainerInterface;
return [
    // This will provide a single connection to your database, deferring the
    // connection until either directly accessed or a service with PDO as a
    // dependency is accessed.
    // Note: you may opt to elide the `ContainerInterface` typehint for brevity
    PDO::class => function (ContainerInterface $c) {
        // This example assumes pdo_dsn, database_user, and database_pass are
        // defined elsewhere (probably using the `env` helper)
        return new PDO(
            $c->get('pdo_dsn'),
            $c->get('database_user'),
            $c->get('database_pass')
        );
    },
];

autowire(?string $classToAutowire = null)

使用 autowire 将使用反射尝试确定指定的类的依赖关系,递归地解析它们,并返回该对象的共享实例。

必需参数必须有一个类型提示才能被解析。这个类型提示可以是类或接口;在两种情况下,这个依赖关系都必须被定义(但也可以自动装配)。不支持具有值类型(标量、数组等)的必需参数,必须手动装配。

可选参数将始终提供其默认值。

重要

具有任何未类型化构造参数的类,或者使用 int/float/bool/array 类型化的类,**不能**被自动装配。

自动装配

在返回的定义数组中,如果没有键的裸字符串值,则将值视为要自动装配的键。

以下都是等效的定义

<?php
/**
class MySpecialClass
{
}
class MyOtherClass
{
    public function __construct(MySpecialClass $required)
    {
        // ...
    }
}
*/
return [
    MySpecialClass::class,
    MyOtherClass::class,
];
<?php
use function Firehed\Container\autowire;
return [
    MySpecialClass::class => autowire(),
    MyOtherClass::class => autowire(),
];
<?php
use function Firehed\Container\autowire;
return [
    MySpecialClass::class => autowire(MySpecialClass::class),
    MyOtherClass::class => autowire(MyOtherClass::class),
];
<?php
return [
    MySpecialClass::class => function () {
        return new MySpecialClass();
    },
    MyOtherClass::class => function (ContainerInterface $c) {
        return new MyOtherClass($c->get(MySpecialClass::class));
    },
];

最高示例推荐用于配置任何可以自动装配的类。

factory(?闭包 $body = null)

使用 factory 在每次通过 get() 访问时返回类的或值的新的副本。

如果没有提供定义的参数,则将键用于自动装配定义。如果提供了闭包,则将执行该闭包。

env(string $variableName, ?string $default = null)

使用 env 将环境变量嵌入到您的容器中。与其他非工厂值一样,这些值将在脚本的整个生命周期中被缓存。

env 集成了一个小型 DSL,允许您以 int、float 或 bool 的形式获取设置在环境中的值,而不是从环境读取的本地字符串。为此,存在以下方法

  • asBool
  • asInt
  • asFloat
  • asEnum

这些方法大致等同于例如 (int) getenv('SOME_ENV_VAR'),但 asBool 只允许值 01"true""false"(不区分大小写)。

asEnum 接受一个到您定义的 基于字符串的 枚举的类字符串,并将使用 ::from($envValue) 从环境值进行初始化。它不会尝试局部规范化值,因此环境变量值必须与支持值完全匹配。

警告

不要使用 getenv$_ENV 来访问环境变量!如果您这样做,编译的容器将获得 编译时 设置的值,这几乎肯定不是您想要的行为。相反,请使用 env 包装器,它将延迟访问环境变量,直到第一次使用。

如果您 想要编译一个值,您必须直接使用 getenv

此类源定义

<?php
use function Firehed\Container\env;
return [
    'some_key' => env('SOME_ENV_VAR'),
    'some_key_with_default' => env('SOME_ENV_VAR', 'default_value'),
    'some_key_with_null_default' => env('SOME_ENV_VAR', null),

    'some_bool' => env('SOME_BOOL')->asBool(),
    'some_int' => env('SOME_INT')->asInt(),
    'some_float' => env('SOME_FLOAT')->asFloat(),
    'some_enum' => env('SOME_ENUM')->asEnum(MyEnum::class),

    // Counterexample!
    'getenv' => getenv('VALUE_AT_COMPILE_TIME'),
];

将编译成类似这样的代码

<?php
return [
    'some_key' => function () {
        $value = getenv('SOME_ENV_VAR');
        if ($value === false) {
            throw new Firehed\Container\Exceptions\EnvironmentVariableNotSet('SOME_ENV_VAR');
        }
        return $value;
    },
    'some_key_with_default' => function () {
        $value = getenv('SOME_ENV_VAR');
        if ($value === false) {
            return 'default_value';
        }
        return $value;
    },
    'some_key_with_null_default' => function () {
        $value = getenv('SOME_ENV_VAR');
        if ($value === false) {
            return null;
        }
        return $value;
    },

    'some_bool' => function () {
        $value = getenv('SOME_ENV_VAR');
        if ($value === false) {
            throw new Firehed\Container\Exceptions\EnvironmentVariableNotSet('SOME_ENV_VAR');
        }
        $value = strtolower($value);
        if ($value === '1' || $value === 'true') {
            return true;
        } elseif ($value === '0' || $value === 'false') {
            return false;
        } else {
            throw new OutOfBoundsException('Invalid boolean value');
        }
    },
    'some_int' => function () {
        $value = getenv('SOME_INT');
        if ($value === false) {
            throw new Firehed\Container\Exceptions\EnvironmentVariableNotSet('SOME_INT');
        }
        return (int)$value;
    },
    'some_float' => function () {
        $value = getenv('SOME_FLOAT');
        if ($value === false) {
            throw new Firehed\Container\Exceptions\EnvironmentVariableNotSet('SOME_FLOAT');
        }
        return (float)$value;
    },
    'some_enum' => function () {
        $value = getenv('SOME_ENUM');
        if ($value === false) {
            throw new Firehed\Container\Exceptions\EnvironmentVariableNotSet('SOME_ENUM');
        }
        return MyEnum::from($value);
    },

    // Counterexample!
    'getenv' => 'whatever_value_is_in_your_current_environment',
];

自动检测

如果您的软件遵循常见的约定,容器引导过程可以大大简化

$container = \Firehed\Container\AutoDetect::from('config');

它将自动检测您的环境,查看 ENVIRONMENTENV 环境变量,按此顺序。如果是 localdevdevelopment(不区分大小写),则将使用 dev 容器,该容器不会被缓存或编译。任何其他值都将运行编译过程,将输出写入 AutoDetect::$compiledOutputPath = 'vendor/compiledConfig.php'。您可以通过更改该变量来更改输出目录(注意 getcwd()!);默认情况下,将其写入 Composer 的 vendor 目录,因为它通常被 gitignore 忽略。