lexide/syringe

Lexide Syringe,Pimple 的配置工具

2.2.6 2021-11-03 10:49 UTC

README

Syringe 允许创建和填充 Pimple DI 容器,容器中包含在配置文件中定义的服务,这与 Symfony 的 DI 模块 类似。

安装

composer require lexide/syringe

入门

创建和设置新容器最简单的方法是使用 Lexide\Syringe\Syringe 类。它需要一个应用程序目录的路径以及相对于该目录的文件路径列表

use Lexide\Syringe\Syringe;

$appDir = __DIR__;
$configFiles = [
    "config/syringe.yml" // add paths to your configuration files here
];

Syringe::init($appDir, $configFiles);
$container = Syringe::createContainer();

配置文件

默认情况下,Syringe 允许配置文件为 JSON 或 YAML 格式。每个文件可以定义参数、服务和要注入容器的标签,这些实体可以在配置的其他部分进行引用。

参数

参数是一个有名称的静态值,可以直接从容器中访问,也可以注入到其他参数或服务中。为了在配置文件中定义参数,它使用 parameters 键,然后声明参数的名称和值。

parameters:
    myParam: "value"

一旦定义,参数就可以在字符串值内部进行引用,方法是在其名称周围加上 % 符号,当字符串值解析时,将插入参数值。这可以在服务参数或其他参数中完成,如下所示

parameters:
    firstName: "Joe"
    lastName: "Bloggs"
    fullName: "%firstName% %lastName%"

参数可以具有任何标量或数组值。数组可以递归地解析;可以将包含对其他参数引用的字符串数组设置为参数,这适用于值和数组键。

parameters:
    myFirstValue: "first"
    mySecondValue: "second"

    myList:
        - "The first value is %myFirstValue%"
        - "The second value is %mySecondValue%
        
    myHash:
        "%myFirstValue%": "%mySecondValue%"

常量

通常,需要将 PHP 常量设置值进行注入。直接将这些值硬编码到 DI 配置中是脆弱的,并且需要维护以保持同步,应尽可能避免。Syringe 通过允许在配置中直接引用 PHP 常量来解决这个问题,方法是使用 ^ 字符包围常量名称。

parameters:
    maxIntValue: "^PHP_INT_MAX^"
    custom: "^MY_CUSTOM_CONSTANT^"
    classConstant: "^MyModule\\MyService::CLASS_CONSTANT^"

当使用类常量时,您需要提供完全限定的类名。由于这必须放在字符串中,所有正斜杠都必须转义,如下例所示。

服务

服务是类的实例,可以将其注入其他服务、参数或值。配置文件通过 services 键定义服务,并为每个条目提供一个 class 键,其中包含要实例化的完全限定类名。对于具有构造函数参数的类,可以通过设置 arguments 键为值、参数或其他服务的列表来指定这些参数,这些参数是构造函数所需的。

services:
    myService:
        class: MyModule\MyService
        arguments:
            - "first constructor argument"
            - 12345
            - false

服务注入

服务可以作为方法参数注入参数或其他服务,方法是使用以 @ 字符为前缀的服务名称进行引用。这可以通过两种方式之一完成

构造函数注入

可以在服务实例化时进行注入,通过设置服务定义的 arguments 键中的引用。这通常用于必需的依赖项。

services:
    injectable:
        class: MyModule\MyDependency

    myService:
        class: MyModule\MyService
        arguments:
            - "@injectable"
            - "%myParam%"

设置器注入

服务也可以通过在服务实例化后调用方法并传入依赖服务作为参数来注入。这种形式对可选依赖很有用。

services:
    injectable:
        class: MyModule\MyDependency

    myService:
        class: MyModule\MyService
        calls:
            -
                method: "setInjectable"
                arguments:
                    - "@injectable"

可以使用 calls 键在服务上运行任何方法,不一定是注入依赖的方法。它们按照定义的顺序执行。

services:
    myService:
        class: MyModule\MyService
        calls:
            - method: "warmCache"
            - method: "setTimeout"
              arguments: ["%myTimeout%"]
            - method: "setLogger"
              arguments: ["@myLogger"]

标签

在某些情况下,您可能想将给定类型的所有服务作为方法参数注入。这可以通过在配置中手动构建服务引用列表来完成,但维护这样的列表既麻烦又耗时。

解决方案是使用标签;允许您将服务标记为集合的一部分,然后通过一个引用注入整个服务集合。

标签通过在名称前加上 # 字符来引用。

services:
    logHandler1:
        ...
        tags:
            - "logHandlers"
            
    logHandler2:
        ...
        tags:
            - "logHandlers"
            
    loggerService:
        ...
        arguments:
            - "#logHandlers"

当标签被解析时,集合将通过一个简单的数字数组传递。父服务将不会知道使用了标签来生成此列表。

工厂

如果您有许多使用相同类或接口的服务需要可用,将服务的创建抽象成一个工厂类可能是有利的,这有助于维护和复用。Syringe提供了两种使用工厂的方法:通过调用工厂类的静态方法,或者通过调用单独的工厂服务的方法。

services:
    newService1:
        class: MyModule\MyService
        factoryClass: MyModule\MyServiceFactory
        factoryMethod: "createdWithStatic"
        
    newService2:
        class: MyModule\MyService
        factoryService: "@myServiceFactory"
        factoryMethod: "createdWithService"
        
    myServiceFactory:
        class: MyModule\MyServiceFactory

如果工厂方法需要参数,您可以使用与正常服务或方法调用相同的方式,通过 arguments 键传递它们。

服务别名

Syringe 允许您使用 aliasOf 键将服务名称别名为指向另一个定义,这在处理其他模块且需要使用自己的服务版本而不是模块的默认版本时很有用。

# [foo.yml]
services:
    default:
        class: MyModule\DefaultService
        ...

# [bar.yml]
services:
    default:
        aliasOf: "@custom"
        
    custom:
        class: MyModule\MyService
        ...

抽象服务

服务往往具有非常相似的定义或包含始终相同的部分。为了减少重复的配置,服务定义可以“扩展”基本定义。这会将两个定义合并在一起。但是,任何键冲突都将采用服务的值而不是基值的值,不过调用列表是合并而不是覆盖。在基本定义中可以定义哪些键没有限制。基本定义必须标记为 abstract 且不能直接用作服务。这些抽象定义可以像在面向对象中继承一样扩展其他定义。

services:
    loggable:
        abstract: true
        calls:
            - method: "setLogger"
            - arguments: "@logger"

    myService:
        class: MyModule\MyService
        extends: "@loggable"            # this will import the "setLogger" call into this service definition
        
    factoriedService:
        abstract: true
        extends: "@loggable"
        factoryClass: MyModule\MyServiceFactory
        factoryMethod: "create"

    myFactoriedService:
        class: MyModule\MyService
        extends: "@factoriedService"    # imports both the factory config and the "setLogger" call

私有服务

对于绝大多数情况,服务从当前模块外部访问没有问题。事实上,这有利于促进模块化设计、服务复用和代码发现。然而,有时数据安全要求服务被锁定,并且不可用于当前模块之外的控制。可以通过在其定义中添加 private 键将服务标记为私有。

services:
    myService:
        ...
        private: true

私有服务仅对具有相同配置别名的其他服务可用,通常在同一模块内。

存根服务

在某些情况下,您可能需要一个应用程序或外部库注入您没有信息的服务,例如具有不属于您库的功能的插件或适配器。

为了使 syringe 能够处理这些情况,您应该创建一个存根服务作为占位符,稍后可以对其进行别名设置。这些作为其他库通过 Syringe 与您的代码交互的钩子或 API。

# library A
services:

    # This service uses the "adapterService" stub
    aService:
        ...
        arguments:
            - "@adapterService"

    adapterService:
        stub: true
        

# library B
services:

    myAdapter:
        ...
        
    # alias "myAdapter" to be the service injected into "library_a.aService"
    library_a.adapterService:
        aliasOf: "@myAdapter"
        

单独的存根服务不能访问或注入;必须在创建使用它们的服务的别名之前。

导入

当你的对象图足够大时,将配置拆分为单独的文件通常很有用;将相关参数和服务放在一起。这可以通过使用imports键来完成。

imports:
    - "loggers.yml"
    - "users.yml"
    - "report/orders.yml"
    - "report/products.yml"
    
services:
    ...

如果任何导入的文件包含重复的键,列表中更靠后的文件将获胜。由于父文件总是最后处理,其服务和参数总是优先于导入的配置。

# [foo.yml]
parameters:
    baz: "from foo"

# [bar.yml]
imports: 
    - "foo.yml"
    
parameters:
    baz: "from bar"
    
# when bar.yml is loaded into Syringe, the "baz" parameter will have a value of "from bar"

环境变量

如果需要,Syringe 允许你在服务器上设置在运行时导入的环境变量。这可以用来为本地开发机器和生产服务器设置不同的参数值,例如。任何以SYRINGE__为前缀的环境变量都将作为参数导入。

配置别名和命名空间

在处理大型对象图时,冲突的服务名称可能成为问题。为了避免这种情况,Syringe 允许你为配置文件设置“别名”或命名空间。在文件内部,可以正常引用服务,但使用不同别名或没有别名的文件需要将别名作为前缀添加到服务名称。这允许你将 DI 配置隔离开来以实现更好的组织,并促进模块化编码。

例如,在设置用于创建容器的配置文件时,可以给两个配置文件foo.ymlbar.yml设置别名。

$configFiles = [
  "foo_alias" => "foo.yml",
  "bar_alias" => "bar.yml"
];

foo.yml可以定义一个服务,fooOne,它注入了同一个文件中的另一个服务,fooTwo,就像正常一样。然而,如果bar.yml中的服务想要注入fooTwo,它必须使用完整的服务引用@foo_alias.fooTwo。同样,如果fooOne想要从bar.yml中注入barOne,它必须使用@bar_alias.barOne作为服务引用。

扩展

有时你可能需要在依赖模块的服务上调用设置器,以便将你的依赖服务作为模块默认服务的一个替换注入。为了做到这一点,你需要使用extensions键。这允许你指定服务并提供要对其执行的调用列表,实际上是将它们附加到服务的自己的calls键上。

# [foo.yml, aliased with "foo_alias"]
services:
    myService:
        class: MyModule\MyService
        ...

# [bar.yml]
services:
    myCustomLogger:
        ...
        
extensions:
    foo_alias.myService:
        - method: "addLogger"
          arguments: "@myCustomLogger"

引用字符

为了识别引用,以下字符被使用:

  • @ - 服务
  • % - 参数
  • # - 标签
  • ^ - 常量

约定

Syringe 不强制执行命名或风格约定,有一个例外。服务名称可以是任何你喜欢的,只要它不以引用字符之一开头,但配置别名总是用.与服务名称分开,例如myAlias.serviceName。因此,在您自己的服务名称中使用.作为分隔符可能很有用,以“命名空间”相关的服务和参数。

parameters:
    database.host: "..."
    database.username: "..."
    database.password: "..."
    
services:
    database.client:
        ...

高级用法

ContainerBuilder

ContainerBuilder类是Syringe的主要组件。它具有几个配置选项,允许您自定义它构建的容器。

配置文件的基路径

为了在特定文件中使用配置,必须将文件的文件路径传递给ContainerBuilder,它将使用加载系统将文件转换为PHP数组。Syringe在加载文件时使用绝对路径,但当你将配置文件路径传递给ContainerBuilder时,这显然不是理想的。

为了解决这个问题,ContainerBuilder允许你设置一个或多个路径作为基路径,这样你可以在设置时使用相对文件路径。例如,对于具有绝对路径/var/www/app/config/syringe.yml的配置文件,你可以设置基路径为/var/www/app,并使用config/syringe.yml作为相对文件路径。

$basePath = "/var/www/app";
$resolver = new Lexide\Syringe\ReferenceResolver();

$builder = new Lexide\Syringe\ContainerBuilder($resolver, [$basePath]);
$builder->addConfigfile("config/syringe.yml");
...

如果您使用多个基本路径,Syringe 将依次在每个基本路径中查找配置文件,因此顺序很重要。

$basePaths = [
    "my-dir/config",    // both these paths contain a file called "foo.yml"
    "my-dir/app"
];
$resolver = new Lexide\Syringe\ReferenceResolver();

$builder = new Lexide\Syringe\ContainerBuilder($resolver, $basePaths);
$builder->addConfigfile("foo.yml");     // will load my-dir/config/foo.yml, as that is the first base path in the list

应用程序根目录

如果您有处理文件的程序,将应用程序的基本目录作为 DI 配置的参数非常有用,这样您就可以确保您使用的任何相对路径都是正确的。 ContainerBuilder 允许您在运行时设置基本目录和参数名称

$builder->setApplicationRootDirectory("my/application/directory", "myParameterName");

如果没有传递密钥,则默认参数名称为 app.dir

容器类

一些使用 Pimple 的项目,例如 Silex,扩展了 Container 类以向其 API 添加功能。Syringe 可以通过允许您设置它实例化的容器类来以这种方式创建自定义容器

$builder->setContainerClass(Silex\Application::class);
$app = $builder->createContainer(); // returns a new Silex Application

加载器

Syringe 可以支持任何可以转换为嵌套 PHP 数组的数据格式。每个配置文件都由加载器系统处理,该系统由一系列 Loader 对象组成,每个对象处理一种数据格式,它将文件内容解码为配置数组。

默认情况下,ContainerBuilder 没有加载器,因此您需要至少添加一个,然后再构建容器

$builder->addLoader(new Lexide\Syringe\Loader\YamlLoader());

自定义加载器

默认情况下,Syringe 支持 YAML 和 JSON 数据格式,但您可以使用任何可以转换为嵌套 PHP 数组的数据格式。转换由 Loader 执行;这是一个类,它接受文件路径,读取文件并解码数据。

要为您选择的数据格式创建一个 Loader,该类需要实现 LoaderInterface 并声明其名称以及它支持的文件扩展名。例如,一个假设的 XML Loader 看起来可能如下所示

use Lexide\Syringe\Loader\LoaderInterface;

class XmlLoader implements LoaderInterface
{
    public function getName()
    {
        return "XML Loader";
    }
    
    public function supports($file)
    {
        return pathinfo($file, PATHINFO_EXTENSION) == "xml";
    }
    
    public function loadFile($file)
    {
        // load and decode the file, returning the configuration array
    }
}

创建后,此类加载器可以通过将其添加到 ContainerBuilder 的常规方式使用。

填充容器

除了创建新的容器之外,ContainerBuilder 还可以通过 populateContainer 方法填充在其他地方创建的现有容器

$container = new Pimple\Container();
$builder->populateContainer($contianer);

方法引用

ContainerBuilder 类有以下可用方法

构造函数

  • __construct(Lexide\Syringe\ReferenceResolver $resolver, array $configPaths = [])

    构造一个 ContainerBuilder 实例,每个 $configPath 都使用 addConfigPath 方法设置

容器

  • createContainer()

    创建一个全新的容器,其中包含已加载到 ContainerBuilder 的配置文件中定义的所有服务

  • populateContainer(Pimple\Container $container)

    根据 createContainer 填充现有容器中的服务

  • setContainerClass($className)

    设置使用 createContainer 时将实例化的类

配置文件

  • addConfigFile($file, $alias = "")

    添加一个新文件路径以从中加载配置,可选地使用别名作为键的前缀

  • addConfigFiles(array $files)

    一次性添加多个配置文件。具有数字键的元素没有别名,否则键用作该文件别名

  $files = [
      "file1.yml",
      "alias_two" => "file2.yml",
      "file3.yml",
      "alias_four" => "file4.yml"
  ]
  • addConfigPath($path)

    注册一个路径作为相对配置文件路径的基础

加载器

  • addLoader(Lexide\Syringe\Loader\LoaderInterface $loader)

    注册一个加载器以添加对特定数据格式的支持

  • removeLoader($name)

    根据名称删除加载器

  • removeLoaderByFile($file)

    删除支持此文件的任何加载器

杂项

  • setApplicationRootDirectory($path, $key = "")

    将目录设置为应用根目录,当处理相对文件路径时非常有用。参数名称将是$key,或者如果$key为空则为app.dir

致谢

由Danny Smart编写(dannysmart@lexide.com)。