[TOC]

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

1. 前言

在看了一些容器实现代码后, 就手痒想要自己实现一个, 因此也就有了本文接下来的内容.

首先, 实现的容器需要具有以下几点特性:

  • 符合PSR-11标准
  • 实现基本的容器存储功能
  • 具有自动依赖解决能力

本项目代码由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
<?phpnamespace 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'=>'1@1.1']);    // 自动实例化UserManager对象    $methodFullName$c->call(UserManager::class.'::'.'register', ['password'=>123, 'email'=>'1@1.1']);    // 调用类的静态方法    [$className, $staticMethodName]$c->call([UserManager::class, 'quickSend'], ['password'=>123, 'email'=>'1@1.1']);    // 使用字符串调用类的静态方法 $staticMethodFullName$c->call(UserManager::class.'::'.'quickSend', ['password'=>123, 'email'=>'1@1.1']);    // [$obj, $methodName] $c->call([new UserManager(new Mailer()), 'register'], ['password'=>123, 'email'=>'1@1.1']);    // [$obj, $staticMethodName]$c->call([new UserManager(new Mailer()), 'quickSend'], ['password'=>123, 'email'=>'1@1.1']);    

4. 未完..不一定续

暂时写到此处.

后续项目最新代码直接在 GitHub 上维护, 该博文后续视评论需求来决定是否补充.