PHP 依賴注入容器實現

一隻賤熊貓發表於2018-09-29

0x00 前言

在看 Laravel 文件的時候發現入門指南的下一章便是核心架構,對於我這種按部就班往下讀的同學這簡直是勸退篇。各種之前沒有接觸過的概念砸得人頭暈,容器便是其中之一。不過在拜讀過幾篇文章後也逐漸理解了容器的作用,所以特此總結一番。

0x01 為何要有容器?

這個問題可以也可以替換為「容器解決了什麼問題?」。在此之前我們需要理解依賴注入這個概念,可以看一下這篇文章:簡單解釋什麼是 依賴注入 和 控制反轉。在實踐依賴注入的時候我們會遇到一個問題,這裡我將通過示例程式碼解釋,程式碼如下:

class Bread
{
}

class Bacon
{
}

class Hamburger
{
    protected $materials;

    public function __construct(Bread $bread, Bacon $bacon)
    {
        $this->materials = [$bread, $bacon];
    }
}

class Cola
{
}

class Meal
{
    protected $food;

    protected $drink;

    public function __construct(Hamburger $hamburger, Cola $cola)
    {
        $this->food  = $hamburger;
        $this->drink = $cola;
    }
}
複製程式碼

上面是按照依賴注入實現的一段程式碼,我們可以看見套餐類(Meal)依賴漢堡類(Hamburger)和可樂類(Cola),並且漢堡類又依賴於麵包類(Bread)和培根類(Bacon)。通過依賴注入能達到鬆耦合的效果但是這也使得例項化一個有多個依賴的類會變得十分麻煩,下面這段程式碼是例項化一個套餐類的示例:

$bread = new Bread();
$bacon = new Bacon();

$hamburger = new Hamburger($bread, $bacon);
$cola = new Cola();

$meal = new Meal($hamburger, $cola);
複製程式碼

可以看見為了獲得一個套餐物件,我們需要先例項化該物件的依賴,如果依賴還存在依賴,我們還需要在例項化依賴的依賴……為了解決這個問題容器就應運而生了,容器的定位就是「管理類的依賴和執行依賴注入的工具」。通過容器我們可以將例項化這個過程給自動化,比如我們可以直接用一行程式碼獲取套餐物件:

$container->get(Meal::class);
複製程式碼

0x01 簡單容器的實現

下面這段程式碼是一個簡單容器的實現:

class Container
{
    /**
     * @var Closure[]
     */
    protected $binds = [];

    /**
     * Bind class by closure.
     *
     * @param string $class
     * @param Closure $closure
     * @return $this
     */
    public function bind(string $class, Closure $closure)
    {
        $this->binds[$class] = $closure;

        return $this;
    }

    /**
     * Get object by class
     *
     * @param string $class
     * @param array $params
     * @return object
     */
    public function make(string $class, array $params = [])
    {
        if (isset($this->binds[$class])) {
            return ($this->binds[$class])->call($this, $this, ...$params);
        }

        return new $class(...$params);
    }
}
複製程式碼

這個容器只有兩個方法 bindmakebind 方法將一個類名和一個閉包進行繫結,然後 make 方法將執行指定類名對應的閉包,並返回該閉包的返回值。我們通過容器的使用示例加深理解:

$container = new Container();

$container->bind(Hamburger::class, function (Container $container) {
    $bread = $container->make(Bread::class);
    $bacon = $container->make(Bacon::class);

    return new Hamburger($bread, $bacon);
});

$container->bind(Meal::class, function (Container $container) {
    $hamburger = $container->make(Hamburger::class);
    $cola      = $container->make(Cola::class);
    return new Meal($hamburger, $cola);
});

// 輸出 Meal
echo get_class($container->make(Meal::class));
複製程式碼

通過上面這個例子我們可以知道 bind 方法傳遞的是一個「返回類名對應的例項化物件」的閉包,而且該閉包還接收該容器作為引數,所以我們還可以在該閉包內使用容器獲取依賴。上面這段程式碼雖然看起來似乎比使用 new 關鍵字還複雜,但實際上對每一個類,我們只需要 bind 一次即可。以後每次需要該物件直接用 make 方法即可,在我們的工程中肯定會節省很多程式碼量。

0x02 通過反射強化容器

「反射」官方手冊 php.net/manual/zh/b…

在上面的的簡單容器的例子裡,我們還需要通過 bind 方法寫好例項化的「指令碼」,那我們試想有沒有一種方法能夠直接生成我們需要的例項呢?其實通過「反射」並在建構函式指定引數的「型別提示類」我們就能實現自動解決依賴的功能。因為通過反射我們可以獲取指定類建構函式所需要的引數和引數型別,所以我們的容器可以自動解決這些依賴。示例程式碼如下:

/**
 * Get object by class
 *
 * @param string $class
 * @param array $params
 * @return object
 */
public function make(string $class, array $params = [])
{
    if (isset($this->binds[$class])) {
        return ($this->binds[$class])->call($this, $this, ...$params);
    }

    return $this->resolve($class);
}

/**
 * Get object by reflection
 *
 * @param $abstract
 * @return object
 * @throws ReflectionException
 */
protected function resolve($abstract)
{
    // 獲取反射物件
    $constructor = (new ReflectionClass($abstract))->getConstructor();
    // 建構函式未定義,直接例項化物件
    if (is_null($constructor)) {
        return new $abstract;
    }
    // 獲取建構函式引數
    $parameters = $constructor->getParameters();
    $arguments  = [];
    foreach ($parameters as $parameter) {
        // 獲得引數的型別提示類
        $paramClassName = $parameter->getClass()->name;
        // 引數沒有型別提示類,丟擲異常
        if (is_null($paramClassName)) {
            throw new Exception('Fail to get instance by reflection');
        }
        // 例項化引數
        $arguments[] = $this->make($paramClassName);
    }

    return new $abstract(...$arguments);
}
複製程式碼

以上程式碼基於只是修改了原容器類的 make 方法,binds 陣列中沒有找到指定類繫結的閉包後執行 resolve 方法。其中 resolve 方法只是簡單的通過反射獲取指定類的建構函式並將其依賴例項化,最後例項化指定類。到了這一步以後我們例項化套餐類就真的只需要一行程式碼了,連配置都不用:-D。

$container->make(Meal::class);
複製程式碼

當然現在這個容器還是相當簡陋的,因為如果指定類依賴標量值(比如:字串,陣列,數值等非物件型別)會直接丟擲異常,也無法指定部分依賴並且如果依賴的是介面的話還會出錯/(ㄒoㄒ)/~~,但這些功能都在一些成熟的容器庫都有。如果感興趣可以去看它們的原始碼,這裡我推薦看 Pipmle 這個專案。

0x03 總結

本文主要介紹了容器的應用場景並實現了一個簡單的容器,通過使用容器我們能夠很方便的解決依賴注入帶來的問題。但是容器也並不是沒有缺點,因為大部分容器都應用了反射技術,這會帶來較大的效能消耗而且通過容器間接生成的例項 IDE 往往不能識別它的型別,所以就不會有自動提示(可以通過寫文件註釋解決)。不過個人感覺引入容器其實還是利大於弊滴(純屬個人感覺)!

PHP 依賴注入容器實現 - 原文地址

相關文章