Hyperf-依赖注入

思考并回答以下问题:

  • 依赖注入的思想,无论什么语言都是相通的。怎么理解?
  • 类的关系及注入是无需显性定义的。怎么理解?

简介

Hyperf默认采用hyperf/di作为框架的依赖注入管理容器,hyperf/di是一个强大的用于管理类的依赖关系并完成自动注入的组件,与传统依赖注入容器的区别在于更符合长生命周期的应用使用、提供了注解及注解注入的支持、提供了无比强大的AOP面向切面编程能力。

绑定对象关系

简单对象注入

通常来说,类的关系及注入是无需显性定义的,这一切Hyperf都会默默的为您完成,我们通过一些代码示例来说明一下相关的用法。

假设我们需要在IndexController内调用UserService类的getInfoById(int $id)方法。

1
2
3
4
5
6
7
8
9
10
namespace App\Service;

class UserService
{
public function getInfoById(int $id)
{
// 我们假设存在一个Info实体
return (new Info())->fill($id);
}
}

通过构造方法注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
namespace App\Controller;

use App\Service\UserService;

class IndexController
{
/**
* @var UserService
*/
private $userService;

// 通过在构造函数的参数上声明参数类型完成自动注入
public function __construct(UserService $userService)
{
$this->userService = $userService;
}

public function index()
{
$id = 1;
// 直接使用
return $this->userService->getInfoById($id);
}
}

注意调用方也就是IndexController必须是由DI创建的对象才能完成自动注入,而Controller默认是由DI创建的,所以可以直接使用构造函数注入,直接new该对象不会生效。

当您希望定义一个可选的依赖项时,可以通过给参数定义为nullable或将参数的默认值定义为null,即表示该参数如果在DI容器中没有找到或无法创建对应的对象时,不抛出异常而是直接使用null来注入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?php
namespace App\Controller;

use App\Service\UserService;

class IndexController
{
/**
* @var null|UserService
*/
private $userService;

// 通过设置参数为 nullable,表明该参数为一个可选参数
public function __construct(?UserService $userService)
{
$this->userService = $userService;
}

public function index()
{
$id = 1;
if ($this->userService instanceof UserService) {
// 仅值存在时 $userService 可用
return $this->userService->getInfoById($id);
}
return null;
}
}

通过@Inject注解注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
namespace App\Controller;

use App\Service\UserService;
use Hyperf\Di\Annotation\Inject;

class IndexController
{
/**
* 通过 `@Inject` 注解注入由 `@var` 注解声明的属性类型对象
*
* @Inject
* @var UserService
*/
private $userService;

public function index()
{
$id = 1;
// 直接使用
return $this->userService->getInfoById($id);
}
}

使用@Inject注解时需use Hyperf\Di\Annotation\Inject;命名空间;

Required参数

Required参数仅可在1.1.0版本或更高版本使用。

@Inject注解存在一个required参数,默认值为true,当将该参数定义为false时,则表明该成员属性为一个可选依赖,当对应@var的对象不存在于DI容器或不可创建时,将不会抛出异常而是注入一个null,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace App\Controller;

use App\Service\UserService;
use Hyperf\Di\Annotation\Inject;

class IndexController
{
/**
* 通过 `@Inject` 注解注入由 `@var` 注解声明的属性类型对象
* 当 UserService 不存在于 DI 容器内或不可创建时,则注入 null
*
* @Inject(required=false)
* @var UserService
*/
private $userService;

public function index()
{
$id = 1;
if ($this->userService instanceof UserService) {
// 仅值存在时 $userService 可用
return $this->userService->getInfoById($id);
}
return null;
}
}

抽象对象注入

基于上面的例子,从合理的角度上来说,Controller面向的不应该直接是一个UserService类,可能更多的是一个UserServiceInterface的接口类,此时我们可以通过config/autoload/dependencies.php来绑定对象关系达到目的。

定义一个接口类:

1
2
3
4
5
6
7
<?php
namespace App\Service;

interface UserServiceInterface
{
public function getInfoById(int $id);
}

UserService实现接口类:

1
2
3
4
5
6
7
8
9
10
11
<?php
namespace App\Service;

class UserService implements UserServiceInterface
{
public function getInfoById(int $id)
{
// 我们假设存在一个 Info 实体
return (new Info())->fill($id);
}
}

在config/autoload/dependencies.php内完成关系配置:

1
2
3
4
<?php
return [
\App\Service\UserServiceInterface::class => \App\Service\UserService::class
];

这样配置后就可以直接通过UserServiceInterface来注入UserService对象了,我们仅通过注解注入的方式来举例,构造函数注入也是一样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
namespace App\Controller;

use App\Service\UserServiceInterface;
use Hyperf\Di\Annotation\Inject;

class IndexController
{
/**
* @Inject
* @var UserServiceInterface
*/
private $userService;

public function index()
{
$id = 1;
// 直接使用
return $this->userService->getInfoById($id);
}
}

工厂对象注入

我们假设UserService的实现会更加复杂一些,在创建UserService对象时构造函数还需要传递进来一些非直接注入型的参数,假设我们需要从配置中取得一个值,然后UserService需要根据这个值来决定是否开启缓存模式。

我们需要创建一个工厂来生成UserService对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php 
namespace App\Service;

use Hyperf\Contract\ConfigInterface;
use Psr\Container\ContainerInterface;

class UserServiceFactory
{
// 实现一个 __invoke() 方法来完成对象的生产,方法参数会自动注入一个当前的容器实例
public function __invoke(ContainerInterface $container)
{
$config = $container->get(ConfigInterface::class);
// 我们假设对应的配置的 key 为 cache.enable
$enableCache = $config->get('cache.enable', false);
// make(string $name, array $parameters = []) 方法等同于 new ,使用 make() 方法是为了允许 AOP 的介入,而直接 new 会导致 AOP 无法正常介入流程
return make(UserService::class, compact('enableCache'));
}
}

UserService 也可以在构造函数提供一个参数接收对应的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
namespace App\Service;

class UserService implements UserServiceInterface
{

/**
* @var bool
*/
private $enableCache;

public function __construct(bool $enableCache)
{
// 接收值并储存于类属性中
$this->enableCache = $enableCache;
}

public function getInfoById(int $id)
{
return (new Info())->fill($id);
}
}

在 config/autoload/dependencies.php 调整绑定关系:

1
2
3
4
<?php
return [
\App\Service\UserServiceInterface::class => \App\Service\UserServiceFactory::class
];

这样在注入 UserServiceInterface 的时候容器就会交由 UserServiceFactory 来创建对象了。

当然在该场景中可以通过 @Value 注解来更便捷的注入配置而无需构建工厂类,此仅为举例

懒加载

Hyperf 的长生命周期依赖注入在项目启动时完成。这意味着长生命周期的类需要注意:

构造函数时还不是协程环境,如果注入了可能会触发协程切换的类,就会导致框架启动失败。

构造函数中要避免循环依赖(比较典型的例子为 Listener 和 EventDispatcherInterface),不然也会启动失败。

目前解决方案是:只在实例中注入 Psr\Container\ContainerInterface ,而其他的组件在非构造函数执行时通过 container 获取。但 PSR-11 中指出:

「用户不应该将容器作为参数传入对象然后在对象中通过容器获得对象的依赖。这样是把容器当作服务定位器来使用,而服务定位器是一种反模式」

也就是说这样的做法虽然有效,但是从设计模式角度来说并不推荐。

另一个方案是使用 PHP 中常用的惰性代理模式,注入一个代理对象,在使用时再实例化目标对象。Hyperf DI 组件设计了懒加载注入功能。

添加 Hyperf\Di\Listener\LazyLoaderBootApplicationListener 到 config/autoload/listeners.php 中。

监听器会监听 BootApplication 事件,自动读取 lazy_loader 配置,并通过 spl_autoload_register 注册懒加载模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

declare(strict_types=1);

return [
Hyperf\Di\Listener\LazyLoaderBootApplicationListener::class,
];
Copy to clipboardErrorCopied
添加 config/autoload/lazy_loader.php 文件并绑定懒加载关系:

<?php
return [
/**
* 格式为:代理类名 => 原类名
* 代理类此时是不存在的,Hyperf会在runtime文件夹下自动生成该类。
* 代理类类名和命名空间可以自由定义。
*/
'App\Service\LazyUserService' => \App\Service\UserServiceInterface::class
];

这样在注入 App\Service\LazyUserService 的时候容器就会创建一个 懒加载代理类 注入到目标对象中了。

1
2
3
4
5
6
7
8
use App\Service\LazyUserService;

class Foo{
public $service;
public function __construct(LazyUserService $sevice){
$this->service = $service;
}
}

您还可以通过注解 @Inject(lazy=true) 注入懒加载代理。通过注解实现懒加载不用创建配置文件。

1
2
3
4
5
6
7
8
9
10
use Hyperf\Di\Annotation\Inject;
use App\Service\UserServiceInterface;

class Foo{
/**
* @Inject(lazy=true)
* @var UserServiceInterface
*/
public $service;
}

注意:当该代理对象执行下列操作时,被代理对象才会从容器中真正实例化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 方法调用
$proxy->someMethod();

// 读取属性
echo $proxy->someProperty;

// 写入属性
$proxy->someProperty = 'foo';

// 检查属性是否存在
isset($proxy->someProperty);

// 删除属性
unset($proxy->someProperty);

注意事项

容器仅管理长生命周期的对象

换种方式理解就是容器内管理的对象都是单例,这样的设计对于长生命周期的应用来说会更加的高效,减少了大量无意义的对象创建和销毁,这样的设计也就意味着所有需要交由 DI 容器管理的对象均不能包含 状态 值。
状态 可直接理解为会随着请求而变化的值,事实上在 协程 编程中,这些状态值也是应该存放于 协程上下文 中的,即 Hyperf\Utils\Context。

短生命周期对象

通过 new 关键词创建的对象毫无疑问的短生命周期的,那么如果希望创建一个短生命周期的对象但又希望通过依赖注入容器注入相关的依赖呢?这是我们可以通过 make(string $name, array $parameters = []) 函数来创建 $name 对应的的实例,代码示例如下:

$userService = make(UserService::class, [‘enableCache’ => true]);
Copy to clipboardErrorCopied
注意仅 $name 对应的对象为短生命周期对象,该对象的所有依赖都是通过 get() 方法获取的,即为长生命周期的对象

获取容器对象

有些时候我们可能希望去实现一些更动态的需求时,会希望可以直接获取到 容器(Container) 对象,在绝大部分情况下,框架的入口类(比如命令类、控制器、RPC 服务提供者等)都是由 容器(Container) 创建并维护的,也就意味着您所写的绝大部分业务代码都是在 容器(Container) 的管理作用之下的,也就意味着在绝大部分情况下您都可以通过在 构造函数(Constructor) 声明或通过 @Inject 注解注入 Psr\Container\ContainerInterface 接口类都能够获得 Hyperf\Di\Container 容器对象,我们通过代码来演示一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace App\Controller;

use Hyperf\HttpServer\Annotation\AutoController;
use Psr\Container\ContainerInterface;

class IndexController
{
/**
* @var ContainerInterface
*/
private $container;

// 通过在构造函数的参数上声明参数类型完成自动注入
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
}

在某些更极端动态的情况下,或者非 容器(Container) 的管理作用之下时,想要获取到 容器(Container) 对象还可以通过 \Hyperf\Utils\ApplicationContext::getContaienr() 方法来获得 容器(Container) 对象。

1
$container = \Hyperf\Utils\ApplicationContext::getContainer();

源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/

namespace Hyperf\Di;

interface MetadataCollectorInterface
{
/**
* Retrieve the metadata via key.
* @param null|mixed $default
*/
public static function get(string $key, $default = null);

/**
* Set the metadata to holder.
* @param mixed $value
*/
public static function set(string $key, $value): void;

/**
* Serialize the all metadata to a string.
*/
public static function serialize(): string;

/**
* Deserialize the serialized metadata and set the metadata to holder.
*/
public static function deserialize(string $metadata): bool;

/**
* Return all metadata array.
*/
public static function list(): array;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?php

declare(strict_types=1);
/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/

namespace Hyperf\Di;

use Hyperf\Utils\Arr;

abstract class MetadataCollector implements MetadataCollectorInterface
{
/**
* Subclass MUST override this property.
*
* @var array
*/
protected static $container = [];

/**
* Retrieve the metadata via key.
* @param null|mixed $default
*/
public static function get(string $key, $default = null)
{
return Arr::get(static::$container, $key) ?? $default;
}

/**
* Set the metadata to holder.
* @param mixed $value
*/
public static function set(string $key, $value): void
{
Arr::set(static::$container, $key, $value);
}

/**
* Determine if the metadata exist.
* If exist will return true, otherwise return false.
*/
public static function has(string $key): bool
{
return Arr::has(static::$container, $key);
}

/**
* Serialize the all metadata to a string.
*/
public static function serialize(): string
{
return serialize(static::$container);
}

/**
* Deserialize the serialized metadata and set the metadata to holder.
*/
public static function deserialize(string $metadata): bool
{
$data = unserialize($metadata);
static::$container = $data;
return true;
}

public static function list(): array
{
return static::$container;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<?php

declare(strict_types=1);

/**
* This file is part of Hyperf.
*
* @link https://www.hyperf.io
* @document https://doc.hyperf.io
* @contact group@hyperf.io
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
*/

namespace Hyperf\Di;

use InvalidArgumentException;
use ReflectionClass;
use ReflectionMethod;
use ReflectionProperty;

class ReflectionManager extends MetadataCollector
{
/**
* @var array
*/
protected static $container = [];

public static function reflectClass(string $className): ReflectionClass
{
if (! isset(static::$container['class'][$className])) {
if (! class_exists($className) && ! interface_exists($className)) {
throw new InvalidArgumentException("Class {$className} not exist");
}
static::$container['class'][$className] = new ReflectionClass($className);
}
return static::$container['class'][$className];
}

public static function reflectMethod(string $className, string $method): ReflectionMethod
{
$key = $className . '::' . $method;
if (! isset(static::$container['method'][$key])) {
// TODO check interface_exist
if (! class_exists($className)) {
throw new InvalidArgumentException("Class {$className} not exist");
}
static::$container['method'][$key] = static::reflectClass($className)->getMethod($method);
}
return static::$container['method'][$key];
}

public static function reflectProperty(string $className, string $property): ReflectionProperty
{
$key = $className . '::' . $property;
if (! isset(static::$container['property'][$key])) {
if (! class_exists($className)) {
throw new InvalidArgumentException("Class {$className} not exist");
}
static::$container['property'][$key] = static::reflectClass($className)->getProperty($property);
}
return static::$container['property'][$key];
}

public static function clear(): void
{
static::$container = [];
}
}
0%