PHP 依賴注入容器實現

0x00 前言

在看 Laravel 文檔的時候發現入門指南的下一章即是核心架構,對於我這種循序漸進往下讀的同窗這簡直是勸退篇。各類以前沒有接觸過的概念砸得人頭暈,容器即是其中之一。不過在拜讀過幾篇文章後也逐漸理解了容器的做用,因此特此總結一番。php

0x01 爲什麼要有容器?

這個問題能夠也能夠替換爲「容器解決了什麼問題?」。在此以前咱們須要理解依賴注入這個概念,能夠看一下這篇文章:簡單解釋什麼是 依賴注入 和 控制反轉。在實踐依賴注入的時候咱們會遇到一個問題,這裏我將經過示例代碼解釋,代碼以下:html

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)。經過依賴注入能達到鬆耦合的效果可是這也使得實例化一個有多個依賴的類會變得十分麻煩,下面這段代碼是實例化一個套餐類的示例:laravel

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

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

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

能夠看見爲了得到一個套餐對象,咱們須要先實例化該對象的依賴,若是依賴還存在依賴,咱們還須要在實例化依賴的依賴……爲了解決這個問題容器就應運而生了,容器的定位就是「管理類的依賴和執行依賴注入的工具」。經過容器咱們能夠將實例化這個過程給自動化,好比咱們能夠直接用一行代碼獲取套餐對象:git

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

0x01 簡單容器的實現

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

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 依賴注入容器實現 - 原文地址

相關文章
相關標籤/搜索