0

0

ThinkPHP6源码:从Http类的实例化看依赖注入是如何实现的

藏色散人

藏色散人

发布时间:2019-08-16 14:28:51

|

3817人浏览过

|

来源于learnku

转载

ThinkPHP6源码:从Http类的实例化看依赖注入是如何实现的

thinkphp 6 从原先的 app 类中分离出 http 类,负责应用的初始化和调度等功能,而 app 类则专注于容器的管理,符合单一职责原则。

以下源码分析,我们可以从 AppHttp 类的实例化过程,了解类是如何实现自动实例化的,依赖注入是怎么实现的。

从入口文件出发


当访问一个 ThinkPHP 搭建的站点,框架最先是从入口文件开始的,然后才是应用初始化、路由解析、控制器调用和响应输出等操作。

入口文件主要代码如下:

立即学习PHP免费学习笔记(深入)”;

// 引入自动加载器,实现类的自动加载功能(PSR4标准)
// 对比Laravel、Yii2、Thinkphp的自动加载实现,它们基本就都一样
// 具体实现可参考我之前写的Laravel的自动加载实现:
// @link: https://learnku.com/articles/20816
require __DIR__ . '/../vendor/autoload.php';
// 这一句和分为两部分分析,App的实例化和调用「http」,具体见下文分析
$http = (new App())->http;
$response = $http->run();
$response->send();
$http->end($response);

App 实例化


执行 new App() 实例化时,首先会调用它的构造函数。

public function __construct(string $rootPath = '')
{
    // thinkPath目录:如,D:\dev\tp6\vendor\topthink\framework\src\
    $this->thinkPath   = dirname(__DIR__) . DIRECTORY_SEPARATOR;
    // 项目根目录,如:D:\dev\tp6\
    $this->rootPath    = $rootPath ? rtrim($rootPath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR : $this->getDefaultRootPath();
    $this->appPath     = $this->rootPath . 'app' . DIRECTORY_SEPARATOR;
    $this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
    // 如果存在「绑定类库到容器」文件
    if (is_file($this->appPath . 'provider.php')) {
        //将文件里的所有映射合并到容器的「$bind」成员变量中
        $this->bind(include $this->appPath . 'provider.php');
    }
    //将当前容器实例保存到成员变量「$instance」中,也就是容器自己保存自己的一个实例
    static::setInstance($this);
    // 保存绑定的实例到「$instances」数组中,见对应分析
    $this->instance('app', $this);
    $this->instance('think\Container', $this);
}

构造函数实现了项目各种基础路径的初始化,并读取了 provider.php 文件,将其类的绑定并入 $bind 成员变量,provider.php 文件默认内容如下:

return [
    'think\Request'          => Request::class,
    'think\exception\Handle' => ExceptionHandle::class,
];

合并后,$bind 成员变量的值如下:

458b98e98133f987e0e22f5bbdf9277.png

$bind 的值是一组类的标识到类的映射。从这个实现也可以看出,我们不仅可以在 provider.php 文件中添加标识到类的映射,而且可以覆盖其原有的映射,也就是将某些核心类替换成自己定义的类。

static::setInstance($this) 实现的作用,如图:

b32bd6edc668f70c4ce89ccd2e7932d.png

think\App 类的 $instance 成员变量指向 think\App 类的一个实例,也就是类自己保存自己的一个实例。

instance() 方法的实现:

public function instance(string $abstract, $instance)
{
    //检查「$bind」中是否保存了名称到实际类的映射,如 'app'=> 'think\App'
    //也就是说,只要绑定了这种对应关系,通过传入名称,就可以找到实际的类
    if (isset($this->bind[$abstract])) {
        //$abstract = 'app', $bind = "think\App"
        $bind = $this->bind[$abstract];
        //如果「$bind」是字符串,重走上面的流程
        if (is_string($bind)) {
            return $this->instance($bind, $instance);
        }
    }
    //保存绑定的实例到「$instances」数组中
    //比如,$this->instances["think\App"] = $instance;
    $this->instances[$abstract] = $instance;
    return $this;
}

执行结果大概是这样的:

MotionGo
MotionGo

AI智能对话式PPT创作,输入内容一键即可完成

下载

92d1e72342faa14f12fb75733d31f31.png

Http 类的实例化以及依赖注入原理


这里,$http = (new App())->http,前半部分好理解,后半部分乍一看有点让人摸不着头脑,App 类并不存在 http 成员变量,这里何以大胆调用了一个不存在的东东呢?

原来,App 类继承自 Container 类,而 Container 类实现了__get() 魔术方法,在 PHP 中,当访问到的变量不存在,就会触发__get() 魔术方法。该方法的实现如下:

public function __get($name)
{
    return $this->get($name);
}

实际上是调用 get() 方法:

public function get($abstract)
{
    //先检查是否有绑定实际的类或者是否实例已存在
    //比如,$abstract = 'http'
    if ($this->has($abstract)) {
        return $this->make($abstract);
    }
    // 找不到类则抛出类找不到的错误
    throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
}

然而,实际上,主要是 make() 方法:

public function make(string $abstract, array $vars = [], bool $newInstance = false)
    {
        //如果已经存在实例,且不强制创建新的实例,直接返回已存在的实例
        if (isset($this->instances[$abstract]) && !$newInstance) {
            return $this->instances[$abstract];
        }
        //如果有绑定,比如 'http'=> 'think\Http',则 $concrete = 'think\Http'
        if (isset($this->bind[$abstract])) {
            $concrete = $this->bind[$abstract];
            if ($concrete instanceof Closure) {
                $object = $this->invokeFunction($concrete, $vars);
            } else {
                //重走一遍make函数,比如上面http的例子,则会调到后面「invokeClass()」处
                return $this->make($concrete, $vars, $newInstance);
            }
        } else {
            //实例化需要的类,比如'think\Http'
            $object = $this->invokeClass($abstract, $vars);
        }
        if (!$newInstance) {
            $this->instances[$abstract] = $object;
        }
        return $object;
    }

然而,然而,make() 方法主要靠 invokeClass() 来实现类的实例化。该方法具体分析:

public function invokeClass(string $class, array $vars = [])
    {
        try {
            //通过反射实例化类
            $reflect = new ReflectionClass($class);
            //检查是否有「__make」方法
            if ($reflect->hasMethod('__make')) {
                //返回的$method包含'__make'的各种信息,如公有/私有
                $method = new ReflectionMethod($class, '__make');
                //检查是否是公有方法且是静态方法
                if ($method->isPublic() && $method->isStatic()) {
                    //绑定参数
                    $args = $this->bindParams($method, $vars);
                    //调用该方法(__make),因为是静态的,所以第一个参数是null
                    //因此,可得知,一个类中,如果有__make方法,在类实例化之前会首先被调用
                    return $method->invokeArgs(null, $args);
                }
            }
            //获取类的构造函数
            $constructor = $reflect->getConstructor();
            //有构造函数则绑定其参数
            $args = $constructor ? $this->bindParams($constructor, $vars) : [];
            //根据传入的参数,通过反射,实例化类
            $object = $reflect->newInstanceArgs($args);
            // 执行容器回调
            $this->invokeAfter($class, $object);
            return $object;
        } catch (ReflectionException $e) {
            throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
        }
    }

以上代码可看出,在一个类中,添加__make() 方法,在类实例化时,会最先被调用。以上最值得一提的是 bindParams() 方法:

protected function bindParams($reflect, array $vars = []): array
{
    //如果参数个数为0,直接返回
    if ($reflect->getNumberOfParameters() == 0) {
        return [];
    }
    // 判断数组类型 数字数组时按顺序绑定参数
    reset($vars);
    $type   = key($vars) === 0 ? 1 : 0;
    //通过反射获取函数的参数,比如,获取Http类构造函数的参数,为「App $app」
    $params = $reflect->getParameters();
    $args   = [];
    foreach ($params as $param) {
        $name      = $param->getName();
        $lowerName = self::parseName($name);
        $class     = $param->getClass();
        //如果参数是一个类
        if ($class) {
            //将类型提示的参数实例化
            $args[] = $this->getObjectParam($class->getName(), $vars);
        } elseif (1 == $type && !empty($vars)) {
            $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() 方法:

protected function getObjectParam(string $className, array &$vars)
{
    $array = $vars;
    $value = array_shift($array);
    if ($value instanceof $className) {
        $result = $value;
        array_shift($vars);
    } else {
        //实例化传入的类
        $result = $this->make($className);
    }
    return $result;
}

getObjectParam() 方法再一次光荣地调用 make() 方法,实例化一个类,而这个类,正是从 Http 的构造函数提取的参数,而这个参数又恰恰是一个类的实例 ——App 类的实例。到这里,程序不仅通过 PHP 的反射类实例化了 Http 类,而且实例化了 Http 类的依赖 App 类。假如 App 类又依赖 C 类,C 类又依赖 D类…… 不管多少层,整个依赖链条依赖的类都可以实现实例化。

总的来说,整个过程大概是这样的:需要实例化 Http 类 ==> 提取构造函数发现其依赖 App 类 ==> 开始实例化 App 类(如果发现还有依赖,则一直提取下去,直到天荒地老)==> 将实例化好的依赖(App 类的实例)传入 Http 类来实例化 Http 类。

这个过程,起个装逼的名字就叫做「依赖注入」,起个摸不着头脑的名字,就叫做「控制反转」。

这个过程,如果退回远古时代,要实例化 Http 类,大概是这样实现的(假如有很多层依赖):

.
.
.
$e = new E();
$d = new D($e);
$c = new D($d);
$app = new App($c);
$http = new Http($app);
.
.
.

这得有多累人。而现代 PHP,交给「容器」就好了。容器还有不少功能,后面再详解。

相关文章

PHP速学教程(入门到精通)
PHP速学教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
Java 桌面应用开发(JavaFX 实战)
Java 桌面应用开发(JavaFX 实战)

本专题系统讲解 Java 在桌面应用开发领域的实战应用,重点围绕 JavaFX 框架,涵盖界面布局、控件使用、事件处理、FXML、样式美化(CSS)、多线程与UI响应优化,以及桌面应用的打包与发布。通过完整示例项目,帮助学习者掌握 使用 Java 构建现代化、跨平台桌面应用程序的核心能力。

14

2026.01.14

php与html混编教程大全
php与html混编教程大全

本专题整合了php和html混编相关教程,阅读专题下面的文章了解更多详细内容。

14

2026.01.13

PHP 高性能
PHP 高性能

本专题整合了PHP高性能相关教程大全,阅读专题下面的文章了解更多详细内容。

33

2026.01.13

MySQL数据库报错常见问题及解决方法大全
MySQL数据库报错常见问题及解决方法大全

本专题整合了MySQL数据库报错常见问题及解决方法,阅读专题下面的文章了解更多详细内容。

18

2026.01.13

PHP 文件上传
PHP 文件上传

本专题整合了PHP实现文件上传相关教程,阅读专题下面的文章了解更多详细内容。

12

2026.01.13

PHP缓存策略教程大全
PHP缓存策略教程大全

本专题整合了PHP缓存相关教程,阅读专题下面的文章了解更多详细内容。

6

2026.01.13

jQuery 正则表达式相关教程
jQuery 正则表达式相关教程

本专题整合了jQuery正则表达式相关教程大全,阅读专题下面的文章了解更多详细内容。

3

2026.01.13

交互式图表和动态图表教程汇总
交互式图表和动态图表教程汇总

本专题整合了交互式图表和动态图表的相关内容,阅读专题下面的文章了解更多详细内容。

44

2026.01.13

nginx配置文件详细教程
nginx配置文件详细教程

本专题整合了nginx配置文件相关教程详细汇总,阅读专题下面的文章了解更多详细内容。

5

2026.01.13

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
ThinkPHP6.x 微实战--十天技能课堂
ThinkPHP6.x 微实战--十天技能课堂

共26课时 | 1.6万人学习

ThinkPHP6.x API接口--十天技能课堂
ThinkPHP6.x API接口--十天技能课堂

共14课时 | 1.1万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号