silktide/syringe

Silktide Syringe,Pimple配置实用工具

3.6.0 2021-12-15 12:13 UTC

README

Syringe 允许创建并填充配置文件中定义的服务,使Pimple DI容器得以创建,其方式与Symfony的DI模块类似。

安装

composer require silktide/syringe

3.0版本的变化

向后兼容性

版本3要求在编译容器之前设置所有参数,因为它预先解决了所有参数。

例如,这在版本2中有效,但在3中无效

parameters:
    foo: "%bar%"
$container = Syringe::build([
    "paths" => [__DIR__]
    "files" => ["file.yml"]
]);
$container["bar"] = "chicken";

这在版本3中有效

parameters:
    foo: "%bar%"
Syringe::build([
    "paths" => [__DIR__]
    "files" => ["file.yml"],
    "parameters" => [
        "bar" => "chicken"
    ]
]);

2.0版本的变化

改进

  1. 现在可以缓存构建的容器,节省了启动时的大量时间
  2. 现在可以通过重复它们来转义令牌(50%可以写成“50%%”作为参数)
  3. 可以使用美元符号($)引用环境变量
  4. 有单元测试 :D
  5. 多字节支持。我没有进行全面测试,但我们现在使用多字节安全函数
  6. 多个文件可以共享相同的命名空间,如下所示:{"my-namespace":["service1.json", "service2.json"]}

向后兼容性

Syringe 2.0几乎是100%重写,功能应该保持基本不变,但背后的代码有很大不同。因此,存在很多向后兼容性,我觉得最好是进行一次彻底的向后兼容,而不是反复进行

  1. 现在需要PHP 7.1
  2. 别名现在通过 :: 而不是 . 表示。这使得验证是否使用了别名变得更加清晰
  3. TagCollection 已重新设计以实现迭代器。这意味着当我们注入类似“#collection”的标签时,它现在将返回一个可迭代对象而不是数组。这意味着我们只有在需要时才会构建服务。我们仍然可以使用 ->getServiceNames 来获取 TagCollection 上的服务名称信息
  4. 现在使用 Syringe::build([]) 原始更新容器。我们不再向最终用户提供几个略微不直观的内部类,而是现在提供一个静态方法,该方法接受配置选项的数组
  5. 容器现在始终作为 Syringe 的一部分生成,而不是公开填充现有容器
  6. 继承彼此的文件现在会在覆盖彼此服务时抛出异常。服务已添加新的参数 override。如果您要将服务添加到容器中,并且清楚它会覆盖现有的服务,您可以设置 override 标志,它将不会引发错误
  7. 已移除私有服务,实际上它们没有任何实际用途,只是增加了复杂性。
  8. 环境变量不再通过将参数前缀为 SYRINGE__FOO 来注入,因为这有点笨拙,而且方向也不对。新的 $ 令牌意味着我们可以像这样注入环境变量作为参数 $foo$
  9. 已移除 IniLoader,该格式不适合 DI。
  10. LoaderInterface 已更新,现在需要类型提示
  11. 我们现在支持通过字符重复来转义特殊令牌(环境、参数、常量)。例如,参数值50%可以写成 '50%%')
  12. 由于我们添加了环境变量的令牌,因此在参数中使用 $ 的任何地方都需要转义(例如:“我花了 $$50 买这件衬衫”)

入门

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

use Silktide\Syringe\Syringe;

$container = \Silktide\Syringe\Syringe::build([
	"files" => ["config/syringe.yml"]
]);

流程

这个版本的代码可能相对直观,但以下是库流程的基本概述,供将来尝试维护它的人参考。

Syringe::build 是库中的入口点,处理配置方面。

MasterConfigBuilder 接收要从中加载的文件和路径列表,递归地解析文件,构建一个有序的 FileConfig 列表,它代表我们请求加载的每个配置文件,无论是通过初始配置、导入还是继承。

这些随后合并成一个 MasterConfig.php 文件,该文件根据它们的权重合并任何服务,权重是根据键是否从一个命名空间化的文件中加载来决定的。

然后,MasterConfig 传递给 CompiledConfigBuilder,它处理别名、构建标签和合并抽象配置。

最后,CompiledConfig 传递给一个 ContainerBuilder,它处理填充 Pimple\\Container

关键生产配置

在生产中使用时,应传递一个 PSR-16 缓存接口(最好是作为缓存参数),如下所示

use Silktide\Syringe\Syringe;

$container = \Silktide\Syringe\Syringe::build([
	"cache" => new FileCache(sys_get_temp_dir())
]);

Syringe 最计算密集的部分(尤其是在使用许多基于 Syringe 的库时)是:1. 不同参数的命名空间化,2. 验证配置文件中的类/方法

通过传递 cache 参数,我们缓存生成的 CompiledConfig 并使用它。这导致代码速度大大提高(大约是原来的 7%)

配置文件

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

参数

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

parameters:
    myParam: "value"

定义后,参数可以在字符串值内部引用,通过在名称周围加上 % 符号,然后在字符串值解析时插入参数的值。这可以在服务参数或其他参数中完成,如下所示

parameters:
    firstName: "Joe"
    lastName: "Bloggs"
    fullName: "%firstName% %lastName%"
    array: ["foo", "bar"]
    object: {"foo":"salad", "bar":"fish"}

参数可以具有任何标量或数组值。

常量

经常需要将设置在 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
	    - {"foo":"salad", "bar":"fish"}

服务注入

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

构造函数注入

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

services:
    injectable:
        class: MyModule\MyDependency

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

setter注入

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

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"

当标签被解析时,集合将通过 TagCollection 传递,可以用作迭代器。你应该将类型提示针对迭代器,而不是 TagCollection,除非你确信你知道自己在做什么。

工厂

如果你有许多服务可用,它们使用相同的类或接口,将创建这些服务的创建抽象到工厂类中可能是有益的,以帮助维护和重用。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,并且不能直接用作服务。这些抽象定义可以以相同的方式扩展其他定义,类似于 OOP 中的继承。

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

懒加载服务

依赖注入可能导致生成大量对象,这些对象可能有昂贵的初始化逻辑但实际上并没有使用。因此,Syringe 提供了对 ProxyManager 的本地支持

services:
  	runner:
		class: MyRunner
		arguments:
			- "@expensiveService"
		
	expensiveService:
		class: MyExpensiveClass
		lazy: true

这将注入一个代理对象,对于 MyExpensiveClass 来说是不可区分的,但防止它被不必要地加载

懒加载跳过析构函数

如果服务有 __destruct,ProxyManager 将创建服务,以确保 __destruct 逻辑按预期触发。这感觉有点反直觉,但这是正确的默认行为。

然而,如果你只是从对象中执行清理,并不是真的希望为了执行清理而创建对象。

因此,我们提供了lazySkipDestruct属性,以便我们克服这种行为

services:
  	runner:
		class: MyRunner
		arguments:
			- "@expensiveService"
		
	expensiveService:
		class: MyExpensiveClass
		lazy: true
		lazySkipDestruct: true

导入

当你的对象图足够大时,将配置拆分为单独的文件通常很有用;将相关的参数和服务放在一起。这可以通过使用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允许你在服务器上设置在运行时导入的环境变量。这可以用于设置本地开发机器和生产服务器不同的参数值,例如。它们可以通过使用&(如&myvar&)这样的令牌来注入,所以

配置别名和命名空间

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

例如,在设置从创建容器的配置文件时,可以给两个配置文件foo.ymlbar.yml分配命名空间

$configFiles = [
  "foo_namespace" => "foo.yml",
  "bar_namespace" => "bar.yml"
];

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

扩展

有时你需要调用依赖模块服务的setter,以便将你的依赖服务作为模块默认服务的替代注入。为了做到这一点,你需要使用extensions键。这允许你指定服务并提供对它的调用列表,本质上是将它们附加到服务的自己的calls键上

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

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

引用字符

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

  • @ - 服务
  • % - 参数
  • # - 标签
  • ^ - 常量
  • & - 环境变量

约定

Syringe不强制执行命名或样式约定,但有一个例外。服务名称可以是任何你喜欢的名称,只要它不以引用字符之一开头,但配置命名空间始终与服务名称通过::分隔,例如myAlias::serviceName,因此,应避免在任何服务/参数名称中使用它

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

高级使用

配置文件的基路径

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

为了解决这个问题,可以向配置数组添加额外的路径。例如,对于具有绝对路径/var/www/app/config/syringe.yml的配置文件,可以设置基路径为/var/www/app,并使用config/syringe.yml作为相对文件路径。

$container = \Silktide\Syringe\Syringe::build([
   "paths" => ["/var/www/app"]
	"files" => ["config/syringe.yml"]
]);

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

应用程序根目录

如果你有处理文件的程序服务,将应用程序的基础目录作为DI配置参数将非常有用,这样你可以确保使用的任何相对路径都是正确的。

$container = \Silktide\Syringe\Syringe::build([
   "appDir" => "my/application/directory", # Application Directory
   "appDirKey" => "myAppParameterKey"
]);

如果没有传递应用程序目录密钥,默认参数名称为app.dir

容器类

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

$container = \Silktide\Syringe\Syringe::build([
   "containerClass" => "Silex\Application::class"
]);

加载器

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

默认情况下,ContainerBuilder加载PHPLoader、YamlLoader和JsonLoader。

自定义加载器

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

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

use Silktide\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
    }
}

创建后,这样的加载器可以通过覆盖$config["loaders"] = [new XmlLoader()]来使用。

致谢

作者