Implement PHP's automatic dependency injection container EasyDI container

不言
Release: 2023-03-24 22:00:01
Original
2279 people have browsed it

这篇文章主要介绍了关于实现PHP的自动依赖注入容器 EasyDI容器,有着一定的参考价值,现在分享给大家,有需要的朋友可以参考一下


  • 1. 前言

  • 2. 项目代码结构

  • 3. 容器完整代码

    • 3.4.1 解决类构造函数依赖

    • 3.4.2 解决 callable 的参数依赖

    • 3.1 容器主要提供方法

    • 3.2 符合PSR-11标准

    • 3.3 容器的基本存储

    • 3.4 自动依赖解决

  • 4. 未完..不一定续


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
Copy after login

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: "\\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); } } }
Copy after login

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 = [])
    从容器中获取

    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']);
Copy after login

4. 未完..不一定续

暂时写到此处.

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

相关推荐:

几行代码轻松实现PHP文件打包下载zip

predis如何实现phpredis的pconnect方法

The above is the detailed content of Implement PHP's automatic dependency injection container EasyDI container. For more information, please follow other related articles on the PHP Chinese website!

Related labels:
source:php.cn
Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
About us Disclaimer Sitemap
php.cn:Public welfare online PHP training,Help PHP learners grow quickly!