今天翻看tp5的源码时,看到了入口文件 index.php 中的 container 类,初看有些不解
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/3fda87e4825953481bb81cb3e4112ca3.png)
通过查看代码与文档,得知是使用了容器来动态的实现类的加载,在开始前,是我们需要了解的几个知识点:
- 注册树模式
- 后期静态绑定
- 自动加载
- 依赖注入
- 控制反转(Ioc)
- 反射机制
- 单例模式
- 闭包函数
我们先定义第一个文件,这里很重要,主要是实现自动加载,当实例化一些不存在的类时根据命名空间去引入
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2020-07-03
* Time: 09:59
*/
//定义根目录路径
define('ROOT_PATH', str_replace('\\','/',dirname(dirname(__FILE__))) );
//自动加载文件
spl_autoload_register('autoLoad',true,true);
function autoLoad($class_name){
$arr = explode('\\', $class_name);
if (count($arr) > 0) {
$path = '';
foreach($arr as $k=> $v){
$path .= '/'.$v;
}
$file = ROOT_PATH."$path.php";
@include_once $file;
}
}
定义好之后,我们新建一个文件夹,并创建一些demo类
其他的 Config.php、demo1.php、demo2.php、demo3.php、Env.php、Log.php 结构与上方的App.php一致
接下来定义我们的容器类 Container.php 吧
<?php
/**
* Created by PhpStorm.
* User: Administrator
* Date: 2020-07-01
* Time: 16:47
*/
namespace Container\basics;
require_once('../Register.php');
class Container
{
/**
* 容器对象实例
* @var Container
*/
protected static $instance;
/**
* 容器中的对象池
* @var array
*/
protected $instances = [];
/**
* 容器绑定标识
* @var array
*/
protected $bind = [
'app' => App::class,
'config' => Config::class,
'env' => Env::class,
'log' => Log::class,
];
/**
* 获取容器实例(单例模式)
* 后期静态绑定
* @return Container
*/
public static function getInstance(){
if (is_null(static::$instance)){
static::$instance = new static;
}
return static::$instance;
}
/**
* 设置当前容器的实例
* 后期静态绑定
* @param $instance
*/
public static function setInstance($instance){
static::$instance = $instance;
}
/**
* 获取当前容器中的实例
* @param $instance
*/
public static function get($instance){
return static::getInstance()->make($instance);
}
/**
* 设置当前容器中的实例
* @param $abstract
* @param null $concrete
*/
public static function set($key,$value){
return static::getInstance()->bindTo($key,$value);
}
/**
* 利用反射机制,往容器中挂载对象
* @param $className
* @param string $key
* @return object
* @throws \ReflectionException
*/
public function make($className){
if (!empty($this->instances[$className])){
$className = $this->instances[$className];
}else if(!empty($this->bind[$className])){
return new $this->bind[$className];
}
$reflect = new \ReflectionClass($className);
$get_construct = $reflect->getConstructor();
if (!$get_construct){
return new $className();
}
$param = $get_construct->getParameters();
if (!$param){
return new $className;
}
foreach($param as $value){
$class = $value->getClass();
if ($class){
$args[] = $this->make($class->name);
}
}
return $reflect->newInstanceArgs($args);
}
/**
* 判断传入参数是对象、命名空间、闭包(暂时只支持三种)
* @param $key
* @param null $value
* @return $this
*/
public function bindTo($key,$value= null){
if (is_object($value)){ //判断值是否为对象
if (!empty($this->bind[$key])){
$key = $this->bind[$key];
}
$this->instances[$key] = $value;
}else if ($value instanceof \Closure){ //判断值是否为闭包
$this->instances[$key] = $value;
}else if(is_string($value)){ //判断是命名空间
if (empty($this->bind[$key])){
$this->instances[$key] = $value;
}else{
$this->instances[$key] = new $this->bind[$key]();
}
} else{ //其他判断
throw new \Exception('该类型不支持挂载,只支持对象或闭包函数');
}
return $this;
}
}
echo '<pre>';
Container::set('app','Container\basics\App');
$res = Container::get('app');
var_dump($res::run());
上面的这个容器,是笔者模仿tp5.1的服务容器,实现的一个简单的容器,下面我们开始解读代码
注册树模式
首先,服务容器的结构,类似于注册树模式,下面是一个简单的注册树模式
class Objpool{
/**
* 存储对象的变量
* @var
*/
private static $pool;
/**
* 往树上挂载对象
* @param $key
* @param $value
*/
public static function set($key,$value){
self::$pool[$key] = $value;
}
/**
* 从树上摘取对象
* @param $key
* @return mixed
*/
public static function get($key){
if (empty(self::$pool[$key])){
self::$pool[$key] = new $key;
}
return self::$pool[$key];
}
/**
* 销毁对象
* @param $key
* @return Exception
*/
public static function _unset($key){
if (empty(self::$pool[$key])){
return new Exception("对象池中不存在该对象");
}
unset(self::$pool[$key]);
}
}
从代码上看,我们的容器是否与注册树模式结构相似? 其实容器就是采用了注册树模式,将对象挂载到树上备用,需要用时再那些来使用,就不用每用到一些类就去new。
后期静态绑定
而Container.php中的 $bind 这里采用的 类名::class,使用 ClassName::class 可以获取一个字符串,包含了类 ClassName 的完全限定名称。 什么意思呢,这里的 APP::class 中的App代表的是App这个类,而App::class 的作用就是拿到这个类下面的命名空间。关系可看下图
继续往下面走,我们可以看到代码中有一些 static::$instance 或者 new static 的语法
- static:: 这个关键字能够让你在继承中,父类与子类调用 方法时引用的类是 B 而不是 A
- 同理 new static new self 都是实例化当前类, 但是new static只有代码所在的类,就是子类如果没有重写的话那么实例化的就是父类。 而new self就是严格的当前类
这里可以参考笔者的另外一篇文章会有更细的讲解 php new static与new self 的区别
自动加载
在项目中,我们类与类文件都分开的,但是文中我们的代码又是直接去new 类名 ,这样子不会报错吗?如下图
我们的容器牵扯到一个自动加载的问题 ,可以利用 spl_autoload_register() 内置函数去解决 ,在容器中,我们是需要根据命名空间去引入文件的,每次当我们实例化不存在的类时,应该都去寻找这个类,相关的资料,大家可以看笔者的这篇文章 spl_autoload_register 实现自动加载 以及多次调用
反射实现对类的方法依赖注入和构造函数依赖注入
笔者在没有看源码前,还以为php的语法上面支持对类的方法依赖注入(解决了像类中的方法传对象的问题),查看后源码才得知,原来是:
- 先通过自动加载,利用反射机制去获取其构造函数的参数
- 再递归调用
- 最后实例化出对象
这里的话,有两个点需要了解,一个是依赖注入,另外一个是控制反转。关于这块知识,大家可以看看 IOC基础,另外笔者在写这篇文章之前,也有写过一篇文章是关于 反射利用反射机制完成动态用户权限(RBAC) 的文章,大家有兴趣的话,也可以看看。
总结:
服务容器是用来管理类依赖于运行依赖注入的工具,它是整个tp的核心,提供整个系统功能及服务的配置,容器字面上的意思就是装东西的物品。
而上面的demo只是服务容器的冰山一角,虽然了解到原理,但要实现完整的容器远远不止这么简单,总而言之,服务容器最大的好处将创建对象的步骤交给容器管理,配合服务提供者使用,能大大降低个个模块的耦合度。