innmind/compose

此软件包已被弃用且不再维护。未建议替代软件包。

服务定位器

1.7.4 2018-08-13 05:49 UTC

This package is auto-updated.

Last update: 2022-02-01 13:12:16 UTC


README

master develop
Scrutinizer Code Quality Scrutinizer Code Quality
Code Coverage Code Coverage
Build Status Build Status

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)
);

这是一个示例,用于描述容器定义的所有主要部分。这里唯一的两个必需键是 servicesexpose

services 键将包含构建你的对象所需的所有定义。定义可以嵌套在子数组中,没有深度限制(如下所示),但最深的数组必须是服务定义(否则容器会报错)。

services:
    foo:
        bar:
            crawler Innmind\Crawler\Crawler: []

expose 键明确指出你可以向外界公开的服务,键是你向世界公开的名称,值是你下面描述的服务定义的引用。

arguments 键是上述 函数 原则的实现。它明确指出在构建库时需要提供的参数。对于每个参数,你需要说明你期望的类型,你可以使用

  • 任何具有 is_{type} 函数(例如:intfloatstring 等...)的对象
  • 任何类名
  • sequenceInnmind\Immutable\Sequence 的快捷方式
  • map<K, V>,它表示你想要一个实现 Innmind\Immutable\MapInterface 的对象,其中 KV 可以是此接口支持的任何内容
  • 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> 将创建一个具有 KV 类型的 Innmind\Compose\Lazy\Map 实例
  • set<T> 将创建一个具有 T 类型的 Innmind\Compose\Lazy\Set 实例
  • stream<T> 将创建一个具有 T 类型的 Innmind\Compose\Lazy\Stream 实例
  • merge 将合并所有参数,仅在参数是 Innmind\Immutable\MapInterfaceInnmind\Immutable\SetInterface 的实例时有效

注意mapsetstream 是延迟加载服务(如果来自您的定义之一)的结构,这意味着这些服务将在第一次操作结构时才被实例化。这样做是为了您可以轻松构建事件监听器、命令总线(或其他)而无需将完整容器注入到这些服务中。

注意:您可以根据自己的需求创建构造函数,请参考 Constructor 接口和 Constructors,它们可以在 loader 中使用。

参数

您可以使用的所有参数模式的全列表

  • 任何原始值
  • ${reference},请参考上面的引用系统
  • ...${reference},这将加载引用并展开它。在编译容器时,允许将单个展开作为最后一个参数(关于这一点下面将详细介绍),但是如果没有编译,则没有限制
  • <{key}, {value}> 这描述了一对元素,其中 keyvalue 可以是任何其他模式;此模式只能与 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文件的示例。

绿色表示暴露的服务,蓝色表示参数,橙色表示你依赖项暴露的服务。