资讯专栏INFORMATION COLUMN

实现PHP的自动依赖注入容器 EasyDI容器

rickchen / 1090人阅读

摘要:年月日前言在看了一些容器实现代码后就手痒想要自己实现一个因此也就有了本文接下来的内容首先实现的容器需要具有以下几点特性符合标准实现基本的容器存储功能具有自动依赖解决能力本项目代码由托管可使用进行安装项目代码结构实现实现实现

[TOC]

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

1. 前言

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

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

符合PSR-11标准

实现基本的容器存储功能

具有自动依赖解决能力

本项目代码由GitHub托管

可使用Composer进行安装 composer require yjx/easy-di

2. 项目代码结构
|-src
    |-Exception
        |-InstantiateException.php (实现PsrContainerContainerExceptionInterface)
        |-InvalidArgumentException.php (实现PsrContainerContainerExceptionInterface)
        |-UnknownIdentifierException.php (实现PsrContainerNotFoundExceptionInterface)
    |-Container.php # 容器
|-tests
    |-UnitTest
        |-ContainerTest.php
3. 容器完整代码
代码版本 v1.0.1
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: "xxxxxx"=>"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(本容器)实现了 PsrContainerContainerInterface 接口, 提供 has($id)get($id, $params=[]) 两个方法用于判断及获取条目.

对于无法解析的条目识别符, 则会抛出异常(实现了 NotFoundExceptionInterface 接口).

3.3 容器的基本存储

容器可用于保存 不被解析的条目, 及自动解析的条目.

不被解析的条目
主要用于保存 配置参数, 已实例化对象, 不被解析的闭包

自动解析的条目
get(...) 时会被容器自动解析, 若是 闭包 则会自动调用, 若是 类名 则会实例化, 若是 别名 则会解析其对应的条目.

3.4 自动依赖解决

EasyDI 在调用 闭包 及 实例化 已经 调用函数/方法(call()) 时能够自动注入所需的依赖, 其中实现的原理是使用了PHP自带的反射API.

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

ReflectionClass

ReflectionFunction

ReflectionMethod

ReflectionParameter

3.4.1 解决类构造函数依赖

解析的一般步骤:

获取类的反射类 $reflectionClass = new ReflectionClass($className)

判断能够实例化 $reflectionClass->isInstantiable()

若能实例化, 则获取对应的构造函数的反射方法类 $reflectionMethod = $reflectionClass->getConstructor()

3.1. 若返回null, 则表示无构造函数可直接跳到*步骤6*
3.2 若返回ReflectionMethod实例, 则开始解析其参数

获取构造函数所需的所有依赖参数类 $reflectionParameters = $reflectionMethod->getParameters

逐个解析依赖参数 $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值, 否则抛出异常(无法处理来依赖)  

若依赖参数为空则调用 $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 EasyDIContainer();

// 输出: "test"
echo $c->call("testFunc")."
";    

// 输出: "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 上维护, 该博文后续视评论需求来决定是否补充.

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/31438.html

相关文章

  • PHP IOC/DI 容器 - 依赖自动注入/依赖单例注入/依赖契约注入/参数关联传值

    摘要:标量参数关联传值依赖是自动解析注入的,剩余的标量参数则可以通过关联传值,这样比较灵活,没必要把默认值的参数放在函数参数最尾部。 更新:github(给个小星星呀) -- 2018-4-11:优化服务绑定方法 ::bind 的类型检查模式 借助 PHP 反射机制实现的一套 依赖自动解析注入 的 IOC/DI 容器,可以作为 Web MVC 框架 的应用容器 1、依赖的自动注入:你只需要...

    Paul_King 评论0 收藏0
  • 又一个强大PHP5.3依赖注入容器

    摘要:现在我们就可以在构造函数或者任何其他通过服务容器注入依赖项的地方使用类型提示注入接口创建一个新的类实例,此处将注入的实例。自动解析构造函数所需的依赖的服务容器实现了接口。 简单的服务容器 一个简单的 php 5.3 依赖注入容器。 项目地址:https://github.com/godruoyi/easy-container Why 目前比较流行的 PHP 容器: Pimple La...

    sf190404 评论0 收藏0
  • 【译文】PHP-DI和依赖注入最佳实践

    摘要:在构造函数中注入依赖性在中作为服务的控制器这是痛苦的,当你有个以上的依赖项,你的构造函数是行样板代码在属性中注入依赖性这是我们建议的解决方案。 PHP-DI是用PHP编写的、强大的和实用的、框架无关的依赖注入容器。这是一个关于如何使用PHP-DI和依赖注入的最佳实践指南。 文章来源于PHP-DI,作者:Matthieu Napoli和贡献者。PHP-DI是用PHP编写的、强大的和实用的...

    ivydom 评论0 收藏0
  • php实现依赖注入(DI)和控制反转(IOC)

    摘要:工厂模式,依赖转移当然,实现控制反转的方法有几种。其实我们稍微改造一下这个类,你就明白,工厂类的真正意义和价值了。虽然如此,工厂模式依旧十分优秀,并且适用于绝大多数情况。 此篇文章转载自laravel-china,chongyi的文章https://laravel-china.org/top...原文地址: http://www.insp.top/learn-lar... ,转载务必保...

    tomato 评论0 收藏0
  • 【modernPHP专题(3)】依赖注入与服务容器

    摘要:而依赖倒置原则的思想是,上层不应该依赖下层,应依赖接口。上面通过构造函数注入对象的方式,就是最简单的依赖注入当然注入不仅可以通过构造函数注入,也可以通过属性注入,上面你可以通过一个来动态为这个属性赋值。 依赖倒置和控制反转是一种编程思想,而依赖注入就是通过服务容器实现这种面向接口或者是面向抽象编程的思想 概念理解 依赖倒置原则 依赖倒置是一种软件设计思想,在传统软件中,上层代码依赖于下...

    terro 评论0 收藏0

发表评论

0条评论

rickchen

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<