[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
<?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 解决类构造函数依赖
解析的一般步骤:
- 获取类的反射类
$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 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 上维护, 该博文后续视评论需求来决定是否补充.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。