dhii/services

一组实用的DI服务实现。

v0.1.1-alpha3 2023-01-31 23:49 UTC

README

Continuous Integration Latest Stable Version Latest Unstable Version

此包提供了一组可用于与PSR-11容器以及实验性的服务提供者规范一起使用的服务工厂和扩展定义实现,以替换通常用于定义的无名函数。

需求

  • PHP >= 7.0 < PHP 8

安装

使用 Composer:

composer require dhii/services

不使用Composer:

  1. 前往 Composer.
  2. 安装它。
  3. 查看“使用Composer”

本包中的所有实现均继承自 Service;一个具有 可调用getDependencies 方法,该方法返回一个键值数组。

工厂

一个简单的实现,使用回调来构造其服务。

与正常匿名函数不同,传递给 Factory 的回调不会获得 ContainerInterface 参数,而是获得与给定依赖键匹配的服务。这允许省略大量琐碎的服务检索代码,更重要的是,可以类型提示服务

new Factory(['dep1', 'dep2'], function(int $dep1, SomeInterface $dep2) {
  // ...
});

大致相当于

function (ContainerInterface $c) {
  $dep1 = $c->get('dep1');
  if (!is_int($dep1)) {
    throw new TypeError(sprintf('Parameter $dep1 must be of type int; %1$s given', is_object($dep1) ? get_class($dep1) : gettype($dep1));
  }

  $dep2 = $c->get('dep2');
  if (!($dep2 instanceof SomeInterface)) {
    throw new TypeError(sprintf('Parameter $dep2 must be of type int; %1$s given', is_object($dep2) ? get_class($dep2) : gettype($dep2));
  }
  // ...
}

这对于所有使用类似机制将已解析的服务传递给其他定义的实现都是正确的。因此,为了简洁起见,将省略类型检查代码。

扩展

Factory 非常相似,但回调还接收来自原始工厂或前扩展的服务实例作为第一个参数。

new Extension(['dep1', 'dep2'], function($prev, $dep1, $dep2) {
  // ...
});

相当于

function (ContainerInterface $c, $prev) {
  $dep1 = $c->get('dep1');
  $dep2 = $c->get('dep2');
  // ...
}

构造函数

Factory 的一个变体,它调用构造函数而不是回调函数。在仅使用其他服务构建类的场景中非常有用。

new Constructor(MyClass::class, ['dep1', 'dep2']);

相当于

function (ContainerInterface $c) {
  $dep1 = $c->get('dep1');
  $dep2 = $c->get('dep2');

  return new MyClass($dep1, $dep2);
}

因此,它也可以在没有任何依赖的情况下工作,这对于构造函数参数为空时非常有用

new Constructor(MyClass::class);

相当于

function (ContainerInterface $c) {
  return new MyClass();
}

服务列表

创建一个包含由其依赖项指示的服务数组。与 ArrayExtension 结合使用时非常有用,可以管理实例的注册。

new ServiceList(['service1', 'service2']);

相当于

function (ContainerInterface $c) {
  return [
    $c->get('service1'),
    $c->get('service2'),
  ];
}

数组扩展

一个扩展实现,将它的依赖项添加到前面的值中。将新实例注册到列表中非常有用。

new ArrayExtension(['dep1', 'dep2'])

相当于

function (ContainerInterface $c, array $prev) {
  return array_merge($prev, [
    $c->get('dep1'),
    $c->get('dep2'),
  ]);
}

函数服务

Factory 的一个变体,但返回回调而不是调用它。注入依赖项之前将传递调用参数。对于声明回调服务非常有用。

new FuncService(['dep1', 'dep2'], function($arg1, $arg2, $dep1, $dep2) {
  // ...
});

相当于

function (ContainerInterface $c) {
  $dep1 = $c->get('dep1');
  $dep2 = $c->get('dep2');

  return function ($arg1, $arg2) use ($dep1, $dep2) {
    // ...
  };
}

其他

  • StringService - 用于返回其他服务插值字符串的服务。
  • Value - 用于总是返回静态值的服务。
  • Alias - 为其他服务提供别名,当原始服务不存在时具有默认功能。
  • GlobalVar - 用于返回全局变量的服务。

变异

withDependencies() 方法允许所有服务实例在保留原始实例不受影响的情况下,与不同的依赖项一起复制。

$service = new Factory(['database'], function ($database) {
  // ...
});

$service2 = $service->withDependencies(['db']);

这使得在运行时甚至构建过程中修改服务依赖项成为可能,这在处理需要重新连接的第三方服务提供商时特别有用。

多实例化

能够使用不同依赖项派生新服务的好处之一是能够多次使用相同的提供者。让我们看看一个例子。

考虑一个将日志写入文件的日志提供者。

class LoggerProvider implements ServiceProviderInterface
{
  public function getFactories()
  {
    return [
      'logger' => new Constructor(FileLogger::class, ['file_path']),
      'file_path' => new Value(sys_get_tmp_dir() . '/log.txt')
    ];
  }
  
  public function getExtensions ()
  {
    return [];
  }
}

我们的应用程序需要保持2个不同的日志文件:一个用于错误,一个用于调试。

简单地使用上述提供者两次是不行的;我们将重新声明 loggerfile_path 服务。

给工厂前缀 允许我们拥有两个提供者实例,但这会破坏依赖关系。如果我们从一个日志提供者中给工厂前缀,使它们成为 debug_loggerdebug_file_path,则 debug_logger 工厂仍然依赖于 file_path,而前缀后它将不再存在。

这就是依赖关系变异的地方。我们可以编写一个 PrefixingProvider 装饰器,它不仅为提供者中的所有服务添加前缀,还为任何依赖项添加前缀。

(以下类为了简洁起见不完整。假设存在构造函数,并初始化其 $prefix$provider 属性).

class PrefixingProvider implements ServiceProviderInterface {
  public function getFactories() {
    $factories = [];

    foreach ($this->provider->getFactories() as $key => $factory) {
      $deps = $factory->getDependencies();
      $newDeps = array_map(fn($dep) => $this->prefix . $dep, $deps);

      $factories[$this->prefix . $key] = $factory->withDependencies($newDeps);
    }
    
    return $factories;
  }
}

现在我们可以创建同一提供者的两个不同版本

$debugLogProvider = new PrefixingProvider('debug_', new LoggerProvider);
$errorLogProvider = new PrefixingProvider('error_', new LoggerProvider);

第一个将提供 debug_loggerdebug_file_path,而第二个将提供 error_loggererror_file_path

静态分析

通过让所有服务声明它们的依赖关系,我们打开了创建一个工具的可能性,该工具可以静态分析服务列表以构建依赖关系图。这张图可以帮助在不运行代码的情况下发现各种潜在问题。这些见解可以揭示

  • 循环依赖
  • 不存在的依赖
  • 工厂相互覆盖,而不是使用扩展
  • 依赖关系链太深
  • 未使用的服务

截至写作时,尚不存在此类工具,但我 确实 计划承担这项任务。