實現PHP的自動依賴注入容器 EasyDI容器

[TOC]

Last-Modified: 2019年5月10日16:15:36

1. 前言

在看了一些容器實現代碼後, 就手癢想要自己實現一個, 因此也就有了本文接下來的內容.

首先, 實現的容器需要具有以下幾點特性:

本項目代碼由GitHub託管

可使用Composer進行安裝 composer require yjx/easy-di

2. 項目代碼結構

|-src
    |-Exception
        |-InstantiateException.php (實現Psr\Container\ContainerExceptionInterface)
        |-InvalidArgumentException.php (實現Psr\Container\ContainerExceptionInterface)
        |-UnknownIdentifierException.php (實現Psr\Container\NotFoundExceptionInterface)
    |-Container.php # 容器
|-tests
    |-UnitTest
        |-ContainerTest.php

3. 容器完整代碼

代碼版本 v1.0.1
<?php
namespace EasyDI;


use EasyDI\Exception\UnknownIdentifierException;
use EasyDI\Exception\InvalidArgumentException;
use EasyDI\Exception\InstantiateException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;

class Container implements ContainerInterface
{
    /**
     * 保存 參數, 已實例化的對象
     * @var array
     */
    private $instance = [];

    private $shared = [];

    private $raw = [];

    private $params = [];

    /**
     * 保存 定義的 工廠等
     * @var array
     */
    private $binding = [];

    public function __construct()
    {
        $this->raw(ContainerInterface::class, $this);
        $this->raw(self::class, $this);
    }


    /**
     * Finds an entry of the container by its identifier and returns it.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @throws NotFoundExceptionInterface  No entry was found for **this** identifier.
     * @throws ContainerExceptionInterface Error while retrieving the entry.
     *
     * @return mixed Entry.
     */
    public function get($id, $parameters = [], $shared=false)
    {
        if (!$this->has($id)) {
            throw new UnknownIdentifierException($id);
        }

        if (array_key_exists($id, $this->raw)) {
            return $this->raw[$id];
        }

        if (array_key_exists($id, $this->instance)) {
            return $this->instance[$id];
        }

        $define = array_key_exists($id, $this->binding) ? $this->binding[$id] : $id;
        if ($define instanceof \Closure) {
            $instance = $this->call($define, $parameters);
        } else {
            // string
            $class = $define;
            $params = (empty($this->params[$id]) ? [] : $this->params[$id]) + $parameters;

            // Case: "\\xxx\\xxx"=>"abc"
            if ($id !== $class && $this->has($class)) {
                $instance = $this->get($class, $params);
            } else {
                $dependencies = $this->getClassDependencies($class, $params);
                if (is_null($dependencies) || empty($dependencies)) {
                    $instance = $this->getReflectionClass($class)->newInstanceWithoutConstructor();
                } else {
                    $instance = $this->getReflectionClass($class)->newInstanceArgs($dependencies);
                }
            }
        }

        if ($shared || (isset($this->shared[$id]) && $this->shared[$id])) {
            $this->instance[$id] = $instance;
        }
        return $instance;
    }

    /**
     * @param callback $function
     * @param array $parameters
     * @return mixed
     * @throws InvalidArgumentException 傳入錯誤的參數
     * @throws InstantiateException
     */
    public function call($function, $parameters=[], $shared=false)
    {
        //參考 http://php.net/manual/zh/function.call-user-func-array.php#121292 實現解析$function

        $class = null;
        $method = null;
        $object = null;
        // Case1: function() {}
        if ($function instanceof \Closure) {
            $method = $function;
        } elseif (is_array($function) && count($function)==2) {
            // Case2: [$object, $methodName]
            if (is_object($function[0])) {
                $object = $function[0];
                $class = get_class($object);
            } elseif (is_string($function[0])) {
                // Case3: [$className, $staticMethodName]
                $class = $function[0];
            }

            if (is_string($function[1])) {
                $method = $function[1];
            }
        } elseif (is_string($function) && strpos($function, '::') !== false) {
            // Case4: "class::staticMethod"
            list($class, $method) = explode('::', $function);
        } elseif (is_scalar($function)) {
            // Case5: "functionName"
            $method = $function;
        } else {
            throw new InvalidArgumentException("Case not allowed! Invalid Data supplied!");
        }

        try {
            if (!is_null($class) && !is_null($method)) {
                $reflectionFunc = $this->getReflectionMethod($class, $method);
            } elseif (!is_null($method)) {
                $reflectionFunc = $this->getReflectionFunction($method);
            } else {
                throw new InvalidArgumentException("class:$class method:$method");
            }
        } catch (\ReflectionException $e) {
//            var_dump($e->getTraceAsString());
            throw new InvalidArgumentException("class:$class method:$method", 0, $e);
        }

        $parameters = $this->getFuncDependencies($reflectionFunc, $parameters);

        if ($reflectionFunc instanceof \ReflectionFunction) {
            return $reflectionFunc->invokeArgs($parameters);
        } elseif ($reflectionFunc->isStatic()) {
            return $reflectionFunc->invokeArgs(null, $parameters);
        } elseif (!empty($object)) {
            return $reflectionFunc->invokeArgs($object, $parameters);
        } elseif (!is_null($class) && $this->has($class)) {
            $object = $this->get($class, [], $shared);
            return $reflectionFunc->invokeArgs($object, $parameters);
        }

        throw new InvalidArgumentException("class:$class method:$method, unable to invoke.");
    }

    /**
     * @param $class
     * @param array $parameters
     * @throws \ReflectionException
     */
    protected function getClassDependencies($class, $parameters=[])
    {
        // 獲取類的反射類
        $reflectionClass = $this->getReflectionClass($class);

        if (!$reflectionClass->isInstantiable()) {
            throw new InstantiateException($class);
        }

        // 獲取構造函數反射類
        $reflectionMethod = $reflectionClass->getConstructor();
        if (is_null($reflectionMethod)) {
            return null;
        }

        return $this->getFuncDependencies($reflectionMethod, $parameters, $class);
    }

    protected function getFuncDependencies(\ReflectionFunctionAbstract $reflectionFunc, $parameters=[], $class="")
    {
        $params = [];
        // 獲取構造函數參數的反射類
        $reflectionParameterArr = $reflectionFunc->getParameters();
        foreach ($reflectionParameterArr as $reflectionParameter) {
            $paramName = $reflectionParameter->getName();
            $paramPos = $reflectionParameter->getPosition();
            $paramClass = $reflectionParameter->getClass();
            $context = ['pos'=>$paramPos, 'name'=>$paramName, 'class'=>$paramClass, 'from_class'=>$class];

            // 優先考慮 $parameters
            if (isset($parameters[$paramName]) || isset($parameters[$paramPos])) {
                $tmpParam = isset($parameters[$paramName]) ? $parameters[$paramName] : $parameters[$paramPos];
                if (gettype($tmpParam) == 'object' && !is_a($tmpParam, $paramClass->getName())) {
                    throw new InstantiateException($class."::".$reflectionFunc->getName(), $parameters + ['__context'=>$context, 'tmpParam'=>get_class($tmpParam)]);
                }
                $params[] = $tmpParam;
//                $params[] = isset($parameters[$paramName]) ? $parameters[$paramName] : $parameters[$pos];
            } elseif (empty($paramClass)) {
            // 若參數不是class類型

                // 優先使用默認值, 只能用於判斷用戶定義的函數/方法, 對系統定義的函數/方法無效, 也同樣無法獲取默認值
                if ($reflectionParameter->isDefaultValueAvailable()) {
                    $params[] = $reflectionParameter->getDefaultValue();
                } elseif ($reflectionFunc->isUserDefined()) {
                    throw new InstantiateException("UserDefined. ".$class."::".$reflectionFunc->getName());
                } elseif ($reflectionParameter->isOptional()) {
                    break;
                } else {
                    throw new InstantiateException("SystemDefined.  ".$class."::".$reflectionFunc->getName());
                }
            } else {
            // 參數是類類型, 優先考慮解析
                if ($this->has($paramClass->getName())) {
                    $params[] = $this->get($paramClass->getName());
                } elseif ($reflectionParameter->allowsNull()) {
                    $params[] = null;
                } else {
                    throw new InstantiateException($class."::".$reflectionFunc->getName()."  {$paramClass->getName()} ");
                }
            }
        }
        return $params;
    }

    protected function getReflectionClass($class, $ignoreException=false)
    {
        static $cache = [];
        if (array_key_exists($class, $cache)) {
            return $cache[$class];
        }

        try {
            $reflectionClass = new \ReflectionClass($class);
        } catch (\Exception $e) {
            if (!$ignoreException) {
                throw new InstantiateException($class, 0, $e);
            }
            $reflectionClass = null;
        }

        return $cache[$class] = $reflectionClass;
    }

    protected function getReflectionMethod($class, $name)
    {
        static $cache = [];

        if (is_object($class)) {
            $class = get_class($class);
        }

        if (array_key_exists($class, $cache) && array_key_exists($name, $cache[$class])) {
            return $cache[$class][$name];
        }
        $reflectionFunc = new \ReflectionMethod($class, $name);
        return $cache[$class][$name] = $reflectionFunc;
    }

    protected function getReflectionFunction($name)
    {
        static $closureCache;
        static $cache = [];

        $isClosure = is_object($name) && $name instanceof \Closure;
        $isString = is_string($name);

        if (!$isString && !$isClosure) {
            throw new InvalidArgumentException("$name can't get reflection func.");
        }

        if ($isString && array_key_exists($name, $cache)) {
            return $cache[$name];
        }

        if ($isClosure) {
            if (is_null($closureCache)) {
                $closureCache = new \SplObjectStorage();
            }
            if ($closureCache->contains($name)) {
                return $closureCache[$name];
            }
        }

        $reflectionFunc = new \ReflectionFunction($name);

        if ($isString) {
            $cache[$name] = $reflectionFunc;
        }
        if ($isClosure) {
            $closureCache->attach($name, $reflectionFunc);
        }

        return $reflectionFunc;
    }


    /**
     * Returns true if the container can return an entry for the given identifier.
     * Returns false otherwise.
     *
     * `has($id)` returning true does not mean that `get($id)` will not throw an exception.
     * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`.
     *
     * @param string $id Identifier of the entry to look for.
     *
     * @return bool
     */
    public function has($id)
    {
        $has = array_key_exists($id, $this->binding) || array_key_exists($id, $this->raw) || array_key_exists($id, $this->instance);
        if (!$has) {
            $reflectionClass = $this->getReflectionClass($id, true);
            if (!empty($reflectionClass)) {
                $has = true;
            }
        }
        return $has;
    }

    public function needResolve($id)
    {
        return !(array_key_exists($id, $this->raw) && (array_key_exists($id, $this->instance) && $this->shared[$id]));
    }

    public function keys()
    {
        return array_unique(array_merge(array_keys($this->raw), array_keys($this->binding), array_keys($this->instance)));
    }

    public function instanceKeys()
    {
        return array_unique(array_keys($this->instance));
    }

    public function unset($id)
    {
        unset($this->shared[$id], $this->binding[$id], $this->raw[$id], $this->instance[$id], $this->params[$id]);
    }

    public function singleton($id, $value, $params=[])
    {
        $this->set($id, $value, $params, true);
    }

    /**
     * 想好定義數組, 和定義普通項
     * @param $id
     * @param $value
     * @param bool $shared
     */
    public function set($id, $value, $params=[], $shared=false)
    {
        if (is_object($value) && !($value instanceof  \Closure)) {
            $this->raw($id, $value);
            return;
        } elseif ($value instanceof \Closure) {
            // no content
        } elseif (is_array($value)) {
            $value = [
                'class' => $id,
                'params' => [],
                'shared' => $shared
                ] + $value;
            if (!isset($value['class'])) {
                $value['class'] = $id;
            }
            $params = $value['params'] + $params;
            $shared = $value['shared'];
            $value = $value['class'];
        } elseif (is_string($value)) {
            // no content
        }
        $this->binding[$id] = $value;
        $this->shared[$id] = $shared;
        $this->params[$id] = $params;
    }

    public function raw($id, $value)
    {
        $this->unset($id);
        $this->raw[$id] = $value;
    }

    public function batchRaw(array $data)
    {
        foreach ($data as $key=>$value) {
            $this->raw($key, $value);
        }
    }

    public function batchSet(array $data, $shared=false)
    {
        foreach ($data as $key=>$value) {
            $this->set($key, $value, $shared);
        }
    }

}

3.1 容器主要提供方法

容器提供方法:

  • raw(string $id, mixed $value)

適用於保存參數, $value可以是任何類型, 容器不會對其進行解析.

  • set(string $id, \Closure|array|string $value, array $params=[], bool $shared=false)

定義服務

  • singleton(string $id, \Closure|array|string $value, array $params=[])

等同調用set($id, $value, $params, true)

  • has(string $id)

判斷容器是否包含$id對應條目

  • get(string $id, array $params = [])

從容器中獲取$id對應條目, 可選參數$params可優先參與到條目實例化過程中的依賴注入

  • call(callable $function, array $params=[])

利用容器來調用callable, 由容器自動注入依賴.

  • unset(string $id)

從容器中移除$id對應條目

3.2 符合PSR-11標準

EasyDI(本容器)實現了 Psr\Container\ContainerInterface 接口, 提供 has($id)get($id, $params=[]) 兩個方法用於判斷及獲取條目.

對於無法解析的條目識別符, 則會拋出異常(實現了 NotFoundExceptionInterface 接口).

3.3 容器的基本存儲

容器可用於保存 不被解析的條目, 及自動解析的條目.

  • 不被解析的條目
    主要用於保存 配置參數, 已實例化對象, 不被解析的閉包
  • 自動解析的條目
    get(...) 時會被容器自動解析, 若是 閉包 則會自動調用, 若是 類名 則會實例化, 若是 別名 則會解析其對應的條目.

3.4 自動依賴解決

EasyDI 在調用 閉包 及 實例化 已經 調用函數/方法(call()) 時能夠自動注入所需的依賴, 其中實現的原理是使用了PHP自帶的反射API.

此處主要用到的反射API如下:

  • ReflectionClass
  • ReflectionFunction
  • ReflectionMethod
  • ReflectionParameter

3.4.1 解決類構造函數依賴

解析的一般步驟:

  1. 獲取類的反射類 $reflectionClass = new ReflectionClass($className)
  2. 判斷能夠實例化 $reflectionClass->isInstantiable()
  3. 若能實例化, 則獲取對應的構造函數的反射方法類 $reflectionMethod = $reflectionClass->getConstructor()

    3.1. 若返回null, 則表示無構造函數可直接跳到*步驟6*
    3.2 若返回ReflectionMethod實例, 則開始解析其參數
  4. 獲取構造函數所需的所有依賴參數類 $reflectionParameters = $reflectionMethod->getParameters
  5. 逐個解析依賴參數 $reflectionParameter

    5.1 獲取參數對應名及位置 `$reflectionParameter->getName()`, `$reflectionParameter->getClass()`
    5.2 獲取參數對應類型 `$paramClass = $reflectionParameter->getClass()`
    5.2.1 若本次解析手動注入了依賴參數, 則根據參數位置及參數名直接使用傳入的依賴參數 Eg. `$container->get($xx, [1=>123, 'e'=>new \Exception()])`
    5.2.2 若參數是標量類型, 若參數有默認值(`$reflectionParameter->isDefaultValueAvailable()`)則使用默認值, 否則拋出異常(無法處理該依賴)
    5.2.3 若參數是 *class* 類型, 若容器可解析該類型, 則由容器自動實例化 `$this->get($paramClass->getName())`, 若無法解析但該參數允許null, 則傳入null值, 否則拋出異常(無法處理來依賴)  
  6. 若依賴參數爲空則調用 $reflectionClass->newInstanceWithoutConstructor(), 否則調用 $reflectionClass->newInstanceArgs($dependencies); //$dependencies爲步驟5中構造的依賴參數數組
具體完整代碼請參照容器類的 getClassDependencies(...) 方法.

3.4.2 解決 callable 的參數依賴

使用 call(...) 來調用 可調用 時, 自動解決依賴同樣類似上述過程, 只是需要區分是 類函數, 類靜態方法 還是 普通方法, 並相應的使用不同的反射類來解析,

具體完整代碼請參照容器類的 call(...) 方法
class UserManager
{
    private $mailer;

    public function __construct(Mailer $mailer)
    {
        $this->mailer = $mailer;
    }

    public function register($email, $password)
    {
        // The user just registered, we create his account
        // ...

        // We send him an email to say hello!
        $this->mailer->mail($email, 'Hello and welcome!');
    }

    public function quickSend(Mailer $mailer, $email, $password)
    {
        $mailer->mail($email, 'Hello and welcome!');
    }
}

function testFunc(UserManager $manager)
{
    return "test";
}

// 實例化容器
$c = new EasyDI\Container();

// 輸出: 'test'
echo $c->call('testFunc')."\n";    

// 輸出: 'test'
echo $c->call(function (UserManager $tmp) {
    return 'test';
});    

// 自動實例化UserManager對象    [$className, $methodName]
$c->call([UserManager::class, 'register'], ['password'=>123, 'email'=>'[email protected]']);    

// 自動實例化UserManager對象    $methodFullName
$c->call(UserManager::class.'::'.'register', ['password'=>123, 'email'=>'[email protected]']);    

// 調用類的靜態方法    [$className, $staticMethodName]
$c->call([UserManager::class, 'quickSend'], ['password'=>123, 'email'=>'[email protected]']);    

// 使用字符串調用類的靜態方法 $staticMethodFullName
$c->call(UserManager::class.'::'.'quickSend', ['password'=>123, 'email'=>'[email protected]']);    

// [$obj, $methodName] 
$c->call([new UserManager(new Mailer()), 'register'], ['password'=>123, 'email'=>'[email protected]']);    

// [$obj, $staticMethodName]
$c->call([new UserManager(new Mailer()), 'quickSend'], ['password'=>123, 'email'=>'[email protected]']);    

4. 未完..不一定續

暫時寫到此處.

後續項目最新代碼直接在 GitHub 上維護, 該博文後續視評論需求來決定是否補充.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章