innmind/ compose
服务定位器
Requires
- php: ~7.2
- innmind/immutable: ~2.7
- innmind/url: ^2.0
- innmind/url-resolver: ^3.0
- psr/container: ^1.0
- symfony/config: ~3.0|^4.0
- symfony/options-resolver: ~3.0|^4.0
- symfony/yaml: ~3.0|~4.0
Requires (Dev)
- giorgiosironi/eris: ^0.9.0
- innmind/graphviz: ^1.1
- innmind/server-control: ^2.1
- innmind/stream: ^1.3
- mnapoli/silly: ^1.7
- phpunit/phpunit: ~6.0
Suggests
- innmind/graphviz: To be able to use the compose binary
- innmind/server-control: To be able to use the compose binary
- innmind/stream: To be able to use the compose binary
- mnapoli/silly: To be able to use the compose binary
README
master |
develop |
---|---|
Compose
是一个依赖注入容器。它源于许多库需要复杂的对象初始化才能工作这一重复出现的问题。通常的方法是在使用 Symfony 时构建一个专门的包,或者使用其他框架的等效方法,但这可能导致包与库不同步。这会导致将库的变化应用到包上需要额外的工作。并且这需要为每个你想要支持的框架都这样做,这可能是令人烦恼的。所有这些都是为了为库的最终用户提供一种使用它的简单方式。
推动此库的第二个原因是所有现有的(我所知的)依赖注入容器都使用一个 共享的全局状态,这意味着所有依赖项/库的初始化都在同一个容器中。这意味着如果你不小心,你很容易耦合库,通过访问不应该暴露的不同库的对象。由于容器没有明确说明可以访问哪些服务,你可能会在不意识到的情况下这样做。最终的问题是,库的更新可能会破坏你的应用程序,因为你依赖于私有内容。
考虑到所有这些,Compose
是基于以下原则构建的
- 将容器视为一个函数
- 构建应用程序应使用组合
注意: Compose
实现了 PSR-11 接口
安装
composer require innmind/compose
使用
第一部分是定义服务。目前,唯一的方式是通过一个 yaml 文件。
示例
arguments: logger: Psr\Log\LoggerInterface http: Innmind\HttpTransport\Transport clock: Innmind\TimeContinuum\TimeContinuumInterface ?? $defaultClock dependencies: amqp path/to/amqp/definition.yml: clock: $clock expose: crawler: $crawler services: crawler Innmind\Crawler\Crawler: - $http - $amqp.client
然后构建容器
use Innmind\Compose\{ ContainerBuilder\ContainerBuilder, Loader\Yaml }; use Innmind\Url\Path; use Innmind\Immutable\Map; $build = new ContainerBuilder(new Yaml); $psr11Container = $build( new Path('definition.yml'), (new Map('string', 'mixed')) ->put('logger', new \Psr\Log\NullLogger) ->put('http', new \Innmind\HttpTransport\GuzzleTransport) );
这是一个示例,用于描述容器定义的所有主要部分。这里唯一的两个必需键是 services
和 expose
。
services
键将包含构建你的对象所需的所有定义。定义可以嵌套在子数组中,没有深度限制(如下所示),但最深的数组必须是服务定义(否则容器会报错)。
services: foo: bar: crawler Innmind\Crawler\Crawler: []
expose
键明确指出你可以向外界公开的服务,键是你向世界公开的名称,值是你下面描述的服务定义的引用。
arguments
键是上述 函数 原则的实现。它明确指出在构建库时需要提供的参数。对于每个参数,你需要说明你期望的类型,你可以使用
- 任何具有
is_{type}
函数(例如:int
、float
、string
等...)的对象 - 任何类名
sequence
,Innmind\Immutable\Sequence
的快捷方式map<K, V>
,它表示你想要一个实现Innmind\Immutable\MapInterface
的对象,其中K
和V
可以是此接口支持的任何内容set<T>
,这表示您需要一个实现Innmind\Immutable\SetInterface
的对象,其中T
可以是此接口支持的任何类型。stream<T>
,这表示您需要一个实现Innmind\Immutable\StreamInterface
的对象,其中T
可以是此接口支持的任何类型。
注意:您可以创建自己的类型,请参阅用于在 Type
接口和 Types
中使用的 loader。
对于每种类型,您可以在其前面加上 ?
,表示它是可选的。因此,当构建容器时,如果不提供它,它将解析为 null
。或者,您可以指定一个参数,以解析为默认值,然后可以使用语法 {type} ?? ${reference}
,其中 reference
是您定义的服务之一的名字。
最后,定义库容器的容器时不应使用 dependencies
键,而仅在定义您的应用程序时使用。它应该在应用程序中使用,因为这是最终的级别,您可以在此处有跨库的依赖关系。在依赖项下定义的每个键/值对都是提供给子容器的参数。提供的依赖项路径相对于您的定义路径,请参阅 测试 以获取具体的示例。您还可以使用语法 @{vendor}/{package}/path/to/container.yml
,这将解析为 composer 包的路径。
引用系统
为了引用一个服务或参数,您使用语法 ${service_or_argument_name}
,但是并非所有内容都可在任何地方访问。允许的引用
- 参数默认值只能是一个您定义的服务,不允许使用依赖项
- 依赖项参数可以是参数、您的服务之一或来自另一个依赖项公开的服务
- 服务参数可以是容器参数、另一个服务或依赖项服务
服务
构造函数
服务定义必须始终遵循 {name} {constructor}
的模式,其中构造函数部分通常是类名,但您不必局限于这一点。以下是所有构造函数模式
- 类名,如前所述
{class}::{method}
描述一个工厂方法$factory->{method}
描述在第一个声明的参数上调用方法,该参数将用作工厂对象map<K, V>
将创建一个具有K
和V
类型的Innmind\Compose\Lazy\Map
实例set<T>
将创建一个具有T
类型的Innmind\Compose\Lazy\Set
实例stream<T>
将创建一个具有T
类型的Innmind\Compose\Lazy\Stream
实例merge
将合并所有参数,仅在参数是Innmind\Immutable\MapInterface
或Innmind\Immutable\SetInterface
的实例时有效
注意:map
、set
和 stream
是延迟加载服务(如果来自您的定义之一)的结构,这意味着这些服务将在第一次操作结构时才被实例化。这样做是为了您可以轻松构建事件监听器、命令总线(或其他)而无需将完整容器注入到这些服务中。
注意:您可以根据自己的需求创建构造函数,请参考 Constructor
接口和 Constructors
,它们可以在 loader 中使用。
参数
您可以使用的所有参数模式的全列表
- 任何原始值
${reference}
,请参考上面的引用系统...${reference}
,这将加载引用并展开它。在编译容器时,允许将单个展开作为最后一个参数(关于这一点下面将详细介绍),但是如果没有编译,则没有限制<{key}, {value}>
这描述了一对元素,其中key
和value
可以是任何其他模式;此模式只能与map
构造函数一起使用@decorated
请参考 堆栈部分
注意:您可以根据自己的需求创建参数模式,请参考 Argument
接口和 Arguments
,它们可以在 loader 中使用。
堆栈
组合是扩展给定接口实现行为的一种很好的方式,Compose
通过 stack
构造函数简化了这些行为。以下是一个命令总线示例
services: command_bus: default Innmind\CommandBus\CommandBus: - $handlersMap logger Innmind\CommandBus\LoggerCommandBus: - @decorated - $logger queueable Innmind\CommandBus\QueueableCommandBus: - @decorated command_bus stack: - $command_bus.queueable - $command_bus.logger - $command_bus.default
在这里,我们创建了一个与 queueable(logger(default))
相等的命令总线。这是通过 @decorated
参数实现的,该参数指示在构建堆栈时替换哪个参数。
重要:显然,您不能直接实例化具有 @decorated
参数的服务定义。
为了简化跨依赖项的组合,您还可以公开一个必须在堆栈中使用的服务定义。例如,您可以这样做(如果上面的服务已经公开)
dependencies: command_bus path/to/command_bus/definition.yml: handlersMap: $handlersMap expose: command_bus: $bus services: buses: dispatch_domain_events My\App\CommandBus\DispatchEvents: - @decorated - $event_bus flush_entities My\App\CommandBus\FlushEntities: - @decorated - $orm bus stack: - $buses.flush_entities - $command_bus.queueable - $buses.dispatch_domain_events - $command_bus.logger - $command_bus.default
这样,您可以在库中定义一个服务,该服务可以进入堆栈,具有与您的库相关的私有依赖关系,并且仍然可以在应用程序中使用它,而无需透露其内部工作原理。
编译
由于解析定义文件和解析所有引用可能会迅速变得复杂,它可能会对您的应用程序的性能产生影响。这就是为什么您可以将容器编译为原始 PHP。您可以这样操作
use Innmind\Compose\{ ContainerBuilder\Cache, Loader\Yaml }; use Innmind\Url\Path; use Innmind\Immutable\Map; $compile = new Cache( new Path('/path/to/cache/directory'), new Yaml ); $psr11Container = $compile( new Path('definition.yml'), new Map('string', 'mixed') );
这将首次调用时构建编译容器到原始 PHP,然后总是会重用其缓存。但是在开发过程中,您希望每次更新服务定义时都更新您的缓存,为此,将 new Cache($path, $loader)
替换为 Cache::onChange($path, $loader)
。
开发者体验
在处理大型应用程序时,很难在脑海中记住整个依赖图,为了轻松解决这个问题,最好有一个依赖关系的可视化表示。这就是为什么 Compose
附带一个 CLI 工具(bin/compose
),可以帮助您将服务作为 Graphviz 表示形式导出。
bin/compose visualize definition.yml
将打印您的服务的 dot 文件,您可以使用选项 -o graph.svg
直接调用 graphviz 来构建 svg 文件(您可以使用 graphviz 支持的任何格式)。
当积极构建您的服务定义时,您可以使用 bin/compose watch definition.yml graph.svg
,这样每当您点击保存按钮时,都会重新构建图文件。
这里是一个amqp.yml
文件的示例。
绿色表示暴露的服务,蓝色表示参数,橙色表示你依赖项暴露的服务。