hyperf/docs/zh-tw/di.md
2021-04-12 13:03:28 +08:00

14 KiB
Raw Blame History

依賴注入

簡介

Hyperf 預設採用 hyperf/di 作為框架的依賴注入管理容器,儘管從設計上我們允許您更換其它的依賴注入管理容器,但我們強烈不建議您更換該元件。
hyperf/di 是一個強大的用於管理類的依賴關係並完成自動注入的元件,與傳統依賴注入容器的區別在於更符合長生命週期的應用使用、提供了 註解及註解注入 的支援、提供了無比強大的 AOP 面向切面程式設計 能力,這些能力及易用性作為 Hyperf 的核心輸出,我們自信的認為該元件是最優秀的。

安裝

該元件預設存在 hyperf-skeleton 專案中並作為主要元件存在,如希望在其它框架內使用該元件可通過下面的命令安裝。

composer require hyperf/di

繫結物件關係

簡單物件注入

通常來說,類的關係及注入是無需顯性定義的,這一切 Hyperf 都會默默的為您完成,我們通過一些程式碼示例來說明一下相關的用法。
假設我們需要在 IndexController 內呼叫 UserService 類的 getInfoById(int $id) 方法。

<?php
namespace App\Service;

class UserService
{
    public function getInfoById(int $id)
    {
        // 我們假設存在一個 Info 實體
        return (new Info())->fill($id);    
    }
}

通過構造方法注入

<?php
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 建立的,所以可以直接使用建構函式注入

當您希望定義一個可選的依賴項時,可以通過給引數定義為 nullable 或將引數的預設值定義為 null,即表示該引數如果在 DI 容器中沒有找到或無法建立對應的物件時,不丟擲異常而是直接使用 null 來注入。(該功能僅在 1.1.0 或更高版本可用)

<?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 註解注入

<?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 註解注入可作用於 DI 建立的(單例)物件,也可作用於通過 new 關鍵詞建立的物件;

使用 @Inject 註解時需 use Hyperf\Di\Annotation\Inject; 名稱空間;

Required 引數

@Inject 註解存在一個 required 引數,預設值為 true,當將該引數定義為 false 時,則表明該成員屬性為一個可選依賴,當對應 @var 的物件不存在於 DI 容器或不可建立時,將不會丟擲異常而是注入一個 null,如下:

<?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 來繫結物件關係達到目的,我們還是通過程式碼來解釋一下。

定義一個介面類:

<?php
namespace App\Service;

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

UserService 實現介面類:

<?php
namespace App\Service;

class UserService implements UserServiceInterface
{
    public function getInfoById(int $id)
    {
        // 我們假設存在一個 Info 實體
        return (new Info())->fill($id);    
    }
}

config/autoload/dependencies.php 內完成關係配置:

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

這樣配置後就可以直接通過 UserServiceInterface 來注入 UserService 物件了,我們僅通過註解注入的方式來舉例,建構函式注入也是一樣的:

<?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 需要根據這個值來決定是否開啟快取模式(順帶一說 Hyperf 提供了更好用的 模型快取 功能)

我們需要建立一個工廠來生成 UserService 物件:

<?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);
        return make(UserService::class, compact('enableCache'));
    }
}

UserService 也可以在建構函式提供一個引數接收對應的值:

<?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 調整繫結關係:

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

這樣在注入 UserServiceInterface 的時候容器就會交由 UserServiceFactory 來建立物件了。

當然在該場景中可以通過 @Value 註解來更便捷的注入配置而無需構建工廠類,此僅為舉例

懶載入

Hyperf 的長生命週期依賴注入在專案啟動時完成。這意味著長生命週期的類需要注意:

  • 建構函式時還不是協程環境,如果注入了可能會觸發協程切換的類,就會導致框架啟動失敗。

  • 建構函式中要避免迴圈依賴(比較典型的例子為 ListenerEventDispatcherInterface),不然也會啟動失敗。

目前解決方案是:只在例項中注入 Psr\Container\ContainerInterface ,而其他的元件在非建構函式執行時通過 container 獲取。但 PSR-11 中指出:

「使用者不應該將容器作為引數傳入物件然後在物件中通過容器獲得物件的依賴。這樣是把容器當作服務定位器來使用,而服務定位器是一種反模式」

也就是說這樣的做法雖然有效,但是從設計模式角度來說並不推薦。

另一個方案是使用 PHP 中常用的惰性代理模式注入一個代理物件在使用時再例項化目標物件。Hyperf DI 元件設計了懶載入注入功能。

新增 config/autoload/lazy_loader.php 檔案並繫結懶載入關係:

<?php
return [
    /**
     * 格式為:代理類名 => 原類名
     * 代理類此時是不存在的Hyperf會在runtime資料夾下自動生成該類。
     * 代理類類名和名稱空間可以自由定義。
     */
    'App\Service\LazyUserService' => \App\Service\UserServiceInterface::class
];

這樣在注入 App\Service\LazyUserService 的時候容器就會建立一個 懶載入代理類 注入到目標物件中了。

use App\Service\LazyUserService;

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

您還可以通過註解 @Inject(lazy=true) 注入懶載入代理。通過註解實現懶載入不用建立配置檔案。

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

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

注意:當該代理物件執行下列操作時,被代理物件才會從容器中真正例項化。

// 方法呼叫
$proxy->someMethod();

// 讀取屬性
echo $proxy->someProperty;

// 寫入屬性
$proxy->someProperty = 'foo';

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

// 刪除屬性
unset($proxy->someProperty);

短生命週期物件

通過 new 關鍵詞建立的物件毫無疑問的短生命週期的,那麼如果希望建立一個短生命週期的物件但又希望使用 建構函式依賴自動注入功能 呢?這時我們可以通過 make(string $name, array $parameters = []) 函式來建立 $name 對應的的例項,程式碼示例如下:

$userService = make(UserService::class, ['enableCache' => true]);

注意僅 $name 對應的物件為短生命週期物件,該物件的所有依賴都是通過 get() 方法獲取的,即為長生命週期的物件,可理解為該物件是一個淺拷貝的物件

獲取容器物件

有些時候我們可能希望去實現一些更動態的需求時,會希望可以直接獲取到 容器(Container) 物件在絕大部分情況下框架的入口類比如命令類、控制器、RPC 服務提供者等)都是由 容器(Container) 建立並維護的,也就意味著您所寫的絕大部分業務程式碼都是在 容器(Container) 的管理作用之下的,也就意味著在絕大部分情況下您都可以通過在 建構函式(Constructor) 宣告或通過 @Inject 註解注入 Psr\Container\ContainerInterface 介面類都能夠獲得 Hyperf\Di\Container 容器物件,我們通過程式碼來演示一下:

<?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) 物件。

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

注意事項

容器僅管理長生命週期的物件

換種方式理解就是容器內管理的物件都是單例,這樣的設計對於長生命週期的應用來說會更加的高效,減少了大量無意義的物件建立和銷燬,這樣的設計也就意味著所有需要交由 DI 容器管理的物件均不能包含 狀態 值。
狀態 可直接理解為會隨著請求而變化的值,事實上在 協程 程式設計中,這些狀態值也是應該存放於 協程上下文 中的,即 Hyperf\Utils\Context

@Inject 注入覆蓋順序

@Inject 覆蓋順序為子類覆蓋 Trait 覆蓋 父類,即 下述 Originfoo 變數為本身注入的 Foo1

同理,假如 Origin 不存在變數 $foo 時,$foo 會被第一個 Trait 完成注入,注入類 Foo2

use Hyperf\Di\Annotation\Inject;

class ParentClass
{
    /**
     * @Inject
     * @var Foo4 
     */
    protected $foo;
}

trait Foo1{
    /**
     * @Inject
     * @var Foo2 
     */
    protected $foo;
}

trait Foo2{
    /**
     * @Inject
     * @var Foo3
     */
    protected $foo;
}

class Origin extends ParentClass
{
    use Foo1;
    use Foo2;
    /**
     * @Inject
     * @var Foo1
     */
    protected $foo;
}