PHP IOC/DI 容器 - 依賴自動注入/依賴單例注入/依賴契約注入/參數關聯傳值

更新:github給個小星星呀php

-- 2018-4-11:優化服務綁定方法 ::bind 的類型檢查模式

藉助 PHP 反射機制實現的一套 依賴自動解析注入 的 IOC/DI 容器,能夠做爲 Web MVC 框架 的應用容器python

一、依賴的自動注入:你只須要在須要的位置注入你須要的依賴便可,運行時容器會自動解析依賴(存在子依賴也能夠自動解析)將對應的實例注入到你須要的位置。git

二、依賴的單例注入:某些狀況下咱們須要保持依賴的全局單例特性,好比 Web 框架中的 Request 依賴,咱們須要將整個請求響應週期中的全部注入 Request 依賴的位置同步爲在路由階段解析完請求體的 Request 實例,這樣咱們在任何位置均可以訪問全局的請求體對象。github

三、依賴的契約注入:好比咱們依賴某 Storage,目前使用 FileStorage 來實現,後期發現性能瓶頸,要改用 RedisStorage 來實現,若是代碼中大量使用 FileStorage 做爲依賴注入,這時候就須要花費精力去改代碼了。咱們可使用接口 Storage 做爲契約,將具體的實現類 FileStorage / RedisStorage 經過容器的綁定機制關聯到 Storage 上,依賴注入 Storage,後期切換存儲引擎只須要修改綁定便可。redis

四、標量參數關聯傳值:依賴是自動解析注入的,剩餘的標量參數則能夠經過關聯傳值,這樣比較靈活,不必把默認值的參數放在函數參數最尾部。這點我仍是蠻喜歡 python 的函數傳值風格的。框架

function foo($name, $age = 27, $sex)
{
    // php 沒辦法 foo($name = 'big cat', $sex = 'male') 這樣傳值
    // 只能 foo('big cat', 27, 'male') 傳值...
    // python 能夠 foo(name = 'big cat', sex = 'male') 很舒服
}

但這也使得個人容器不支持位序傳值,必須保證運行參數的鍵名與運行方法的參數名準確的關聯映(有默認值的參數能夠省略),我想着並無什麼不方便的地方吧,我不喜歡給 $bar 參數傳遞個 $foo 變量。ide

容器源碼

<?php
/*----------------------------------------------------------------------------------------------------
 | @author big cat
 |----------------------------------------------------------------------------------------------------
 | IOC 容器
 | 一、自動解析依賴     自動的對依賴進行解析,實例化,注入
 |                     /------------------------------------------------------------------------------
 |                     | 好比你用 Redis 或 File 作引擎存儲 Session,能夠定義一個頂層契約接口 Storage
 | 二、契約注入---------| 將具體的實現類 RedisStorage or FileStorage 的實例綁定到此契約
 |                     | 依賴此契約進行注入 後期能夠靈活的更換或者擴展新的存儲引擎
 |                     \-------------------------------------------------------------------------------
 | 三、單例注入         能夠將依賴綁定爲單例,實現此依賴的同步
 | 四、關聯參數傳值     標量參數採用關聯傳值,可設定默認值
 | 備註:關聯傳參才舒服, ($foo = 'foo', $bar), 跳過 foo 直接給 bar 傳值多舒服
 |-----------------------------------------------------------------------------------------------------
 | public static methods:
 |   singleton // 單例服務綁定
 |   bind      // 服務綁定
 |   run       // 運行容器
 | private static methods:
 |   getParam    // 獲取依賴參數
 |   getInstance // 獲取依賴實例
 |-----------------------------------------------------------------------------------------------------
 */

class IOCContainer
{
    /**
     * 註冊到容器內的依賴--服務
     * 能夠經過 singleton($alias, $instance) 綁定全局單例依賴
     * 能夠經過 bind($alias, $class_name) 綁定頂層契約依賴
     * 容器解析依賴時會優先檢查是否爲註冊的內部依賴 如不是則加載外部依賴類實例化後注入
     * @var array
     */
    public static $dependencyServices = array();

    /**
     * 單例模式服務註冊
     * 將具體的實例綁定到服務 整個生命週期中此服務的各處依賴注入都用此實例
     * @param  [type] $service  綁定的服務別名
     * @param  [type] $provider 服務提供者:具體的實例或可實例的類
     * @return [type]                   [description]
     */
    public static function singleton($service, $provider)
    {
        static::bind($service, $provider, true);
    }

    /**
     * 服務註冊
     * 註冊依賴服務到容器內 容器將優先使用此類服務 能夠實現契約注入
     * 契約注入:A Interface 能夠做爲 B Class 和 C Class 的代理人(契約者)注入 B Class 或 C Class 的實例
     * 具體看你綁定的誰 能夠靈活切換底層具體的實現代碼
     * @param  [type]  $service    [description]
     * @param  [type]  $provider   [description]
     * @param  boolean $singleton     [description]
     * @return [type]                 [description]
     */
    public static function bind($service, $provider, $singleton = false)
    {
        // 單例綁定服務提供者必須爲服務的實例 以便全局單例綁定
        if ($singleton && ! is_object($provider)) {
            throw new Exception("service provider must be an instance of $provider!", 4041);
        }
    
        // 若非單例則校驗服務提供者是否存在
        if (! $singleton && ! class_exists($provider)) {
            throw new Exception("service provider not exists!", 4042);
        }
    
        // singleton 標識服務是否爲單例模式
        // 單例場景則 provider 爲具體的實例 不然爲某提供者類
        static::$dependencyServices[$service] = [
            'provider'  => $provider,
            'singleton' => $singleton,
        ];
    }

    /**
     * 獲取類實例
     * 經過反射獲取構造參數
     * 返回對應的類實例
     * @param  [type] $class_name [description]
     * @return [type]             [description]
     */
    private static function getInstance($class_name)
    {
        //方法參數分爲 params 和 default_values
        //若是一個開放構造類做爲依賴注入傳入它類,咱們應該將此類註冊爲全局單例服務
        $params = static::getParams($class_name);
        return (new ReflectionClass($class_name))->newInstanceArgs($params['params']);
    }

    /**
     * 反射方法參數類型
     * 對象參數:構造對應的實例 同時檢查是否爲單例模式的實例
     * 標量參數:返回參數名 索引路由參數取值
     * 默認值參數:檢查路由參數中是否存在本參數 無則取默認值
     * @param  [type] $class_name [description]
     * @param  string $method     [description]
     * @return [type]             [description]
     */
    private static function getParams($class_name, $method = '__construct')
    {
        $params_set['params'] = array();
        $params_set['default_values'] = array();

        //反射檢測類是否顯示聲明或繼承父類的構造方法
        //若無則說明構造參數爲空
        if ($method == '__construct') {
            $classRf = new ReflectionClass($class_name);
            if (! $classRf->hasMethod('__construct')) {
                return $params_set;
            }
        }

        //反射方法 獲取參數
        $methodRf = new ReflectionMethod($class_name, $method);
        $params = $methodRf->getParameters();

        if (! empty($params)) {
            foreach ($params as $key => $param) {
                if ($paramClass = $param->getClass()) {// 對象參數 獲取對象實例
                    $param_class_name = $paramClass->getName();
                    if (array_key_exists($param_class_name, static::$dependencyServices)) {// 是否爲註冊的服務
                        if (static::$dependencyServices[$param_class_name]['singleton']) {// 單例模式直接返回已註冊的實例
                            $params_set['params'][] = static::$dependencyServices[$param_class_name]['provider'];
                        } else {// 非單例則返回提供者的新的實例
                            $params_set['params'][] = static::getInstance(static::$dependencyServices[$param_class_name]['provider']);
                        }
                    } else {// 沒有作綁定註冊的類
                        $params_set['params'][] = static::getInstance($param_class_name);
                    }
                } else {// 標量參數 獲取變量名做爲路由映射 包含默認值的記錄默認值
                    $param_name = $param->getName();

                    if ($param->isDefaultValueAvailable()) {// 是否包含默認值
                        $param_default_value = $param->getDefaultValue();
                        $params_set['default_values'][$param_name] = $param_default_value;
                    }

                    $params_set['params'][] = $param_name;
                }
            }
        }

        return $params_set;
    }

    /**
     * 容器的運行入口 主要負責加載類方法,並將運行所需的標量參數作映射和默認值處理
     * @param  [type] $class_name 運行類
     * @param  [type] $method     運行方法
     * @param  array  $params     運行參數
     * @return [type]             輸出
     */
    public static function run($class_name, $method, array $params = array())
    {
        if (! class_exists($class_name)) {
            throw new Exception($class_name . "not found!", 4040);
        }

        if (! method_exists($class_name, $method)) {
            throw new Exception($class_name . "::" . $method . " not found!", 4041);
        }

        // 獲取要運行的類
        $classInstance = static::getInstance($class_name);
        // 獲取要運行的方法的參數
        $method_params = static::getParams($class_name, $method);
        
        // 關聯傳入的運行參數
        $method_params = array_map(function ($param) use ($params, $method_params) {
            if (is_object($param)) {// 對象參數 以完成依賴解析的具體實例
                return $param;
            }

            // 如下爲關聯傳值 可經過參數名映射的方式關聯傳值 可省略含有默認值的參數
            if (array_key_exists($param, $params)) {// 映射傳遞路由參數
                return $params[$param];
            }

            if (array_key_exists($param, $method_params['default_values'])) {// 默認值
                return $method_params['default_values'][$param];
            }

            throw new Exception($param . ' is necessary parameters', 4042); // 路由中沒有的則包含默認值
        }, $method_params['params']);

        // 運行
        return call_user_func_array([$classInstance, $method], $method_params);
    }
}

演示所需的依賴類

// 它將被以單例模式注入 全局的全部注入點都使用的同一實例
class Foo
{
    public $msg = "foo nothing to say!";

    public function index()
    {
        $this->msg = "foo hello, modified by index method!";
    }
}

// 它將以普通依賴模式注入 各注入點會分別獲取一個實例
class Bar
{
    public $msg = "bar nothing to say!";

    public function index()
    {
        $this->msg = "bar hello, modified by index method!";
    }
}

// 契約注入
interface StorageEngine
{
    public function info();
}

// 契約實現
class FileStorageEngine implements StorageEngine
{
    public $msg = "file storage engine!" . PHP_EOL;

    public function info()
    {
        $this->msg =  "file storage engine!" . PHP_EOL;
    }
}

// 契約實現
class RedisStorageEngine implements StorageEngine
{
    public $msg = "redis storage engine!" . PHP_EOL;

    public function info()
    {
        $this->msg =  "redis storage engine!" . PHP_EOL;
    }
}

演示所需的運行類

// 具體的運行類
class BigCatController
{
    public $foo;
    public $bar;

    // 這裏自動注入一次 Foo 和 Bar 的實例
    public function __construct(Foo $foo, Bar $bar)
    {
        $this->foo = $foo;
        $this->bar = $bar;
    }

    // 這裏的參數你徹底能夠亂序的定義(我故意寫的很亂序),你只需保證 route 參數中存在對應的必要參數便可
    // 默認值參數能夠直接省略
    public function index($name = "big cat", Foo $foo, $sex = 'male', $age, Bar $bar, StorageEngine $se)
    {
        // Foo 爲單例模式注入 $this->foo $foo 是同一實例
        $this->foo->index();
        echo $this->foo->msg . PHP_EOL;
        echo $foo->msg . PHP_EOL;
        echo "------------------------------" . PHP_EOL;

        // Bar 爲普通模式注入 $this->bar $bar 爲兩個不一樣的 Bar 的實例
        $this->bar->index();
        echo $this->bar->msg . PHP_EOL;
        echo $bar->msg . PHP_EOL;
        echo "------------------------------" . PHP_EOL;

        // 契約注入 具體看你爲契約者綁定了哪一個具體的實現類
        // 咱們綁定的 RedisStorageEngine 因此這裏注入的是 RedisStorageEngine 的實例
        $se->info();
        echo $se->msg;
        echo "------------------------------" . PHP_EOL;

        // 返回個值
        return "name " . $name . ', age ' . $age . ', sex ' . $sex . PHP_EOL;
    }
}

運行

// 路由信息很 MVC 吧
$route = [
    'controller' => BigCatController::class, // 運行的類
    'action'     => 'index', // 運行的方法
    'params'     => [ // 運行的參數
        'name' => 'big cat',
        'age'  => 27 // sex 有默認值 不傳
    ]
];

try {
    // 依賴的單例註冊
    IOCContainer::singleton(Foo::class, new Foo());

    // 依賴的契約註冊 StorageEngine 至關於契約者 註冊關聯具體的實現類
    // IOCContainer::bind(StorageEngine::class, FileStorageEngine::class);
    IOCContainer::bind(StorageEngine::class, RedisStorageEngine::class);
    
    // 運行
    $result = IOCContainer::run($route['controller'], $route['action'], $route['params']);
    
    echo $result;
} catch (Exception $e) {
    echo $e->getMessage();
}

運行結果

foo hello, modified by index method!
foo hello, modified by index method!
------------------------------
bar hello, modified by index method!
bar nothing to say!
------------------------------
redis storage engine!
------------------------------
name big cat, age 27, sex male

簡單的實現了像 laraval 的 IOC 容器的特性,但比它多一項(可能也比較雞肋)標量參數的關聯傳值,不過我這功能也限定死了你傳入的參數必須與函數定義的參數名相關聯,可我仍是以爲能充分的填補默認參數不放在參數尾就沒法跳過的強迫症問題.....函數

相關文章
相關標籤/搜索