firehed / container
具有高级自动装配和编译支持的依赖倒置容器。符合PSR-11规范。
Requires
- php: ^8.1
- nikic/php-parser: ^4.7.0 || ^5.0
- psr/container: ^1.0 || ^2.0
- psr/log: ^1.1 || ^2.0 || ^3.0
Requires (Dev)
- phpstan/phpstan: ^1.0
- phpstan/phpstan-phpunit: ^1.0
- phpstan/phpstan-strict-rules: ^1.0
- phpunit/phpunit: ^10.5.27
- squizlabs/php_codesniffer: ^3.5
Provides
This package is auto-updated.
Last update: 2024-08-22 20:43:59 UTC
README
符合PSR-11规范的依赖倒置容器
为什么还需要另一种容器实现方式?
创建此容器的首要动机是为了拥有一种针对长时间运行过程(如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
只允许值 0
、1
、"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');
它将自动检测您的环境,查看 ENVIRONMENT
或 ENV
环境变量,按此顺序。如果是 local
、dev
或 development
(不区分大小写),则将使用 dev 容器,该容器不会被缓存或编译。任何其他值都将运行编译过程,将输出写入 AutoDetect::$compiledOutputPath = 'vendor/compiledConfig.php'
。您可以通过更改该变量来更改输出目录(注意 getcwd()
!);默认情况下,将其写入 Composer 的 vendor
目录,因为它通常被 gitignore
忽略。