TP框架的Container源码分析
版本:5.1.39TLS
Container的结构导图
Container中的属性
- $instance——用于容器自身实例化
- $instances——一个用于存放容器中的对象实例的数组
- $name——容器表示别名
- $bind——容器绑定标识
$bind的初始值
/**
* 容器绑定标识
* @var array
*/
protected $bind = [
'app' => App::class,
'build' => Build::class,
'cache' => Cache::class,
'config' => Config::class,
'cookie' => Cookie::class,
'debug' => Debug::class,
'env' => Env::class,
'hook' => Hook::class,
'lang' => Lang::class,
'log' => Log::class,
'middleware' => Middleware::class,
'request' => Request::class,
'response' => Response::class,
'route' => Route::class,
'session' => Session::class,
'template' => Template::class,
'url' => Url::class,
'validate' => Validate::class,
'view' => View::class,
'rule_name' => route\RuleName::class,
// 接口依赖注入
'think\LoggerInterface' => Log::class,
];
Container所继承的接口
ArrayAccess——实现对象以数组形式访问的接口
Countable——实现count方法或得当前容器中对象实例个数
//Countable
public function count()
{
return count($this->instances);
}
IteratorAggregate——聚合式迭代器接口,让对象组可以被foreach遍历,在Container中遍历容器中的对象实例
public function getIterator()
{
//这个迭代器允许在遍历数组和对象时删除和更新值与键。
return new ArrayIterator($this->instances);
}
Container中的魔术方法
__debugInfo()——在var_dump()类对象的时候被调用,如果没有定义该方法,则var_dump会打印出所有的类属性
public function __debugInfo()
{
//去除其中的容器对象本身和容器中的实例化对象组
$data = get_object_vars($this);
unset($data['instances'], $data['instance']);
return $data;
}
Container中的主要方法分析
getInstance——单例模式思想
/**
* 获取当前容器的实例(单例)
* @access public
* @return static
*/
public static function getInstance()
{
if (is_null(static::$instance)) {
static::$instance = new static;
}
return static::$instance;
}
setInstance
我的理解是提供了一个后门用于更换当前实例对象
/**
* 设置当前容器的实例
* @access public
* @param object $instance
* @return void
*/
public static function setInstance($instance)
{
static::$instance = $instance;
}
make(get)方法
以入口文件index.php的get('app')为例
在执行get('app')时会通过get方法调用make方法,传入的参数为$abstract为'app',其余两个为空。
在make方法中先判断$vars是否为true,如果是则一直创建新的实例对象,但在本例中不是true,故略过
之后判断其是否有别名,本例中'app'没有别名,略过
之后判断在当前容器对象中是否有app的对象实例,因为入口文件为初始化,所以没有实例,略过
之后判断是否存在app的绑定标识,标识存在,对应值为App::class,走里面的逻辑(App::class的值为App的'命名空间/App')
将App::class赋值给$concrete
判断$concrete是否为闭包函数类,可知不是,则走else的逻辑
将$concrete的值赋值给$thiis->name['App'],等于为其设置了一个别名,这样在之后访问时可以直接通过别名获得其对象实例(因为存放实例对象时键为$concrete)
接下来是关键的一步,递归的调用make方法,将$concrete的值作为新的$abstract传入,这时会发现$abstract(APP::class)并没有容器标识,此时走else逻辑,利用invokeClass方法实例化一个App的对象,因为$newInstance为false,所以不用每次创建新的实例化对象,所以将这个对象存入$this->instances中方便下一次使用,返会对象,实现get方法的获取对象
/**
* 获取容器中的对象实例
* @access public
* @param string $abstract 类名或者标识
* @param array|true $vars 变量
* @param bool $newInstance 是否每次创建新的实例
* @return object
*/
public static function get($abstract, $vars = [], $newInstance = false)
{
return static::getInstance()->make($abstract, $vars, $newInstance);
}
/**
* 创建类的实例
* @access public
* @param string $abstract 类名或者标识
* @param array|true $vars 变量
* @param bool $newInstance 是否每次创建新的实例
* @return object
*/
public function make($abstract, $vars = [], $newInstance = false)
{
if (true === $vars) {
// 总是创建新的实例化对象
$newInstance = true;
$vars = [];
}
$abstract = isset($this->name[$abstract]) ? $this->name[$abstract] : $abstract;
if (isset($this->instances[$abstract]) && !$newInstance) {
return $this->instances[$abstract];
}
if (isset($this->bind[$abstract])) {
$concrete = $this->bind[$abstract];
//Closure 闭包函数类
if ($concrete instanceof Closure) {
$object = $this->invokeFunction($concrete, $vars);
} else {
$this->name[$abstract] = $concrete;
return $this->make($concrete, $vars, $newInstance);
}
} else {
$object = $this->invokeClass($abstract, $vars);
}
if (!$newInstance) {
$this->instances[$abstract] = $object;
}
return $object;
}
set(bindTo)方法(使用了注册树模式思想)
bindTo方法先判断$abstract是否为数组,如果是数组则直接将其合并入$bind中
如果不是,判断第二个参数$concrete是否为闭包函数类,如果是则以$abstract为键名绑定到$bind中
如果不是则判断是否为对象,如果是则先判断$abstract是否在Container对象中已经有对应标识,如果有则将标识赋值给$abstract,以$abstract为键值,将$concrete注册到对象组中
如果不是对象,则直接添加到容器标识中
最后返回Container实例,所以这个方法支持链式操作
/**
* 绑定一个类、闭包、实例、接口实现到容器
* @access public
* @param string $abstract 类标识、接口
* @param mixed $concrete 要绑定的类、闭包或者实例
* @return Container
*/
public static function set($abstract, $concrete = null)
{
return static::getInstance()->bindTo($abstract, $concrete);
}
/**
* 绑定一个类、闭包、实例、接口实现到容器
* @access public
* @param string|array $abstract 类标识、接口
* @param mixed $concrete 要绑定的类、闭包或者实例
* @return $this
*/
public function bindTo($abstract, $concrete = null)
{
if (is_array($abstract)) {
$this->bind = array_merge($this->bind, $abstract);
} elseif ($concrete instanceof Closure) {
$this->bind[$abstract] = $concrete;
} elseif (is_object($concrete)) {
if (isset($this->bind[$abstract])) {
$abstract = $this->bind[$abstract];
}
$this->instances[$abstract] = $concrete;
} else {
$this->bind[$abstract] = $concrete;
}
return $this;
}
remove和clear比较简单就不做分析了
invokeFunction方法
在之前的make方法中可以看到当$concrete为闭包函数时调用这个方法
根据所传的函数实例化了一个ReflectionFunction类(php方法反射类)的对象,这个对象包含了这个函数的相关信息
然后调用bindParams[]方法绑定参数,最后使用call_user_func_array方法调用函数
/**
* 执行函数或者闭包方法 支持参数调用
* @access public
* @param mixed $function 函数或者闭包
* @param array $vars 参数
* @return mixed
*/
public function invokeFunction($function, $vars = [])
{
try {
$reflect = new ReflectionFunction($function);
$args = $this->bindParams($reflect, $vars);
//call_user_func_array — 调用回调函数,并把一个数组参数作为回调函数的参数
return call_user_func_array($function, $args);
} catch (ReflectionException $e) {
throw new Exception('function not exists: ' . $function . '()');
}
}
invokeMethod方法
流程与invokeFunction大致一致,注意的是$menthod可以传数组,但这个数组为[类(或对象), 方法],且只会取第一个方法
/**
* 调用反射执行类的方法 支持参数绑定
* @access public
* @param mixed $method 方法
* @param array $vars 参数
* @return mixed
*/
public function invokeMethod($method, $vars = [])
{
try {
if (is_array($method)) {
$class = is_object($method[0]) ? $method[0] : $this->invokeClass($method[0]);
$reflect = new ReflectionMethod($class, $method[1]);
} else {
// 静态方法
$reflect = new ReflectionMethod($method);
}
$args = $this->bindParams($reflect, $vars);
return $reflect->invokeArgs(isset($class) ? $class : null, $args);
} catch (ReflectionException $e) {
if (is_array($method) && is_object($method[0])) {
$method[0] = get_class($method[0]);
}
throw new Exception('method not exists: ' . (is_array($method) ? $method[0] . '::' . $method[1] : $method) . '()');
}
}
invokeReflectMethod方法
/**
* 调用反射执行类的方法 支持参数绑定
* @access public
* @param object $instance 对象实例
* @param mixed $reflect 反射类
* @param array $vars 参数
* @return mixed
*/
public function invokeReflectMethod($instance, $reflect, $vars = [])
{
$args = $this->bindParams($reflect, $vars);
return $reflect->invokeArgs($instance, $args);
}
invoke方法
将invokeFunction和invokeMethod方法整合在了一起,可以通过这个方法直接使用
/**
* 调用反射执行callable 支持参数绑定
* @access public
* @param mixed $callable
* @param array $vars 参数
* @return mixed
*/
public function invoke($callable, $vars = [])
{
if ($callable instanceof Closure) {
return $this->invokeFunction($callable, $vars);
}
return $this->invokeMethod($callable, $vars);
}
invokeClass方法——使用了反射的机制,依赖注入的思想
由外部传入的类名生成对应的类体现了依赖注入的思想
这个方法会先根据类名生成一个反射类
若这个反射类有__make()方法,则使用此方法直接返回实例对象
若没有则获取其构造函数,判断其是否有参数并且最终返回这个类的实例化对象
/**
* 调用反射执行类的实例化 支持依赖注入
* @access public
* @param string $class 类名
* @param array $vars 参数
* @return mixed
*/
public function invokeClass($class, $vars = [])
{
try {
$reflect = new ReflectionClass($class);
if ($reflect->hasMethod('__make')) {
$method = new ReflectionMethod($class, '__make');
if ($method->isPublic() && $method->isStatic()) {
$args = $this->bindParams($method, $vars);
return $method->invokeArgs(null, $args);
}
}
$constructor = $reflect->getConstructor();
$args = $constructor ? $this->bindParams($constructor, $vars) : [];
return $reflect->newInstanceArgs($args);
} catch (ReflectionException $e) {
throw new ClassNotFoundException('class not exists: ' . $class, $class);
}
}
bindParams方法
传入了一个(方法|函数)反射类和一组参数
以下均以方法举例
先判断这个反射类反射的方法是否含有参数,没有就直接返回空数组
然后用reset保证数组$vars指针指向第一个元素,判断该数组的类型,数字数组type为1,反之为2
通过getParameters获取参数赋值给$params
遍历$params获取其参数名称和转为c语言风格的参数名称
判断它是否有类型提示类,如果有则通过getObjectParam获得它的实例化对象为参数,赋值给$args
若没有则判断它是否为数字数组并且$vars是否不为空,如果都满足则将$vars的第一个元素输出并且删除第一个元素
若不满足则判断$vars是否存在参数名称对应的值若存在则将对应值添加到$args中
若不存在对应值,在获取其默认值添加至$args中
若都没有,则说明参数缺失,抛出参数缺失的异常
/**
* 绑定参数
* @access protected
* @param \ReflectionMethod|\ReflectionFunction $reflect 反射类
* @param array $vars 参数
* @return array
*/
protected function bindParams($reflect, $vars = [])
{
if ($reflect->getNumberOfParameters() == 0) {
return [];
}
// 判断数组类型 数字数组时按顺序绑定参数
// reset 把数组的内部指针重置到数组中的第一个元素:
reset($vars);
$type = key($vars) === 0 ? 1 : 0;
$params = $reflect->getParameters();
foreach ($params as $param) {
$name = $param->getName();
$lowerName = Loader::parseName($name);
$class = $param->getClass();
if ($class) {
$args[] = $this->getObjectParam($class->getName(), $vars);
} elseif (1 == $type && !empty($vars)) {
//array_shift 输出第一个元素并且删除
$args[] = array_shift($vars);
} elseif (0 == $type && isset($vars[$name])) {
$args[] = $vars[$name];
} elseif (0 == $type && isset($vars[$lowerName])) {
$args[] = $vars[$lowerName];
} elseif ($param->isDefaultValueAvailable()) {
$args[] = $param->getDefaultValue();
} else {
throw new InvalidArgumentException('method param miss:' . $name);
}
}
return $args;
}
getObjectParam方法
这里的逻辑比较简单,但要注意这里的$vars使用的是引用传值
/**
* 获取对象类型的参数值
* @access protected
* @param string $className 类名
* @param array $vars 参数
* @return mixed
*/
protected function getObjectParam($className, &$vars)
{
$array = $vars;
$value = array_shift($array);
if ($value instanceof $className) {
$result = $value;
array_shift($vars);
} else {
$result = $this->make($className);
}
return $result;
}
相关助手函数
app()——快速获取容器中的实例,支持依赖注入
if (!function_exists('app')) {
/**
* 快速获取容器中的实例 支持依赖注入
* @param string $name 类名或标识 默认获取当前应用实例
* @param array $args 参数
* @param bool $newInstance 是否每次创建新的实例
* @return mixed|\think\App
*/
function app($name = 'think\App', $args = [], $newInstance = false)
{
return Container::get($name, $args, $newInstance);
}
}
bind()——绑定一个类到容器(这里不是太理解为什么不使用set方法)
if (!function_exists('bind')) {
/**
* 绑定一个类到容器
* @access public
* @param string $abstract 类标识、接口
* @param mixed $concrete 要绑定的类、闭包或者实例
* @return Container
*/
function bind($abstract, $concrete = null)
{
return Container::getInstance()->bindTo($abstract, $concrete);
//return Container::set($abstract, $concrete);
}
}
container()——绑定一个类的实例
if (!function_exists('container')) {
/**
* 获取容器对象实例
* @return Container
*/
function container()
{
return Container::getInstance();
}
}