• ThinkPHP框架执行流程源码解析


    本文主要介绍框架的执行流程

    前言

    如果不清楚框架是怎么执行的,那么看在多的代码都是只是认识代码而已,阅读源码是为了学习其框架的设计思想和代码模式。

    而执行流程则是将我们学习的东西串联在一起,从而更好地理解。咔咔也会给大家把执行流程用思维导图的方式画出来。

    只要大家在本文学习到一点点的知识点,咔咔也是心满意足的。

    这个流程图只是针对initialize的执行过程,其余的执行过程后期会进行补充,都是以脑图的形式呈现给大家的。

    在这里插入图片描述
    在这里插入图片描述

    一、框架执行流程之初始化应用的数据设置

    这里的内容跟容器的内容有点重复,因为执行流程是从入口文件开始的,并且最后也是通过容器执行的。

    入口文件
    入口文件

    然后就会进入到文件thinkphp/library/think/App.php的run方法,在这个方法中主要就是下图框出来的地方,执行的initialize方法。

    thinkphp/library/think/App.php 来到initialize这个方法,先看上半部分。

    • microtime(true);返回的是unix的微秒数
    • memory_get_usage返回的是分配给PHP的内存量,单位为字节
    • 在接下来就是对框架的几个路径进行设置
    • static::setInstance($this);这里是将app这个实例设置为容器实例
    • $this->instance('app', $this);这个在之前容器章节就提到了,就是为了把app这个类绑定到容器里边去,也就是注册树模式。

    初始化应用上半段 这里有一个小的问题点给大家提出来,在初始化应用的这个方法里边存在这样一行代码。

    有没有小伙伴对这个$this->env和下边的$this->config这俩个调用有疑惑。

    如果你有疑惑那就跟着咔咔一起来看,没疑惑的就可以继续往下看了。

    App这个类是继承的容器类,那么这个env和config不论是在app还是container类中都是没有这俩个属性的。

    那么怎么就可以直接调用呢!而且代码追踪都会追踪到env类和container类中。

    需要知道这个源头就需要我们去在大致的看一遍container类的代码。

    解决疑惑,为什么可以这样使用
    解决疑惑,为什么可以这样使用

    经过一番苦读之后,可以看到下图的几行代码。这几行代码全部使用的是魔术方法。

    当访问env类不存在的时候就会去执行make方法。

    make这个方法在容器那一章节进行的细的不能再细的解读了。

    这个make方法最终会返回一个类的实例,并且还会存到容器里边。

    容器类中的魔术方法 这里只放一个make方法的代码,如果有不会的可以去看之前的文章。

    容器类的make方法 最后就是加载一系列的数据,加载详情请看前言的思维导图。

    执行加载
    执行加载

    二、如何查看一个方法都在哪里执行了

    在阅读源码的过程中,有一个很难把控的问题就是一个方法在不同的地方进行了调用,但是咱们确一时半会根本不知道都在哪里调用了。

    这里用init方法来做一个演示。

    init方法是初始化应用或者模块的一个方法,但是这里的module参数确实一个空值。

    init初始化应用 先做一个断点查看一下相关的数据信息。

    打印的结果就是空,这就是一些新学习的伙伴会犯的一个错误,因为这个方法不可能只调用一次的。

    如果初始化模块都是空那么这个方法就没有存在的必要了。

    断点查看module的值 断点打印结果 那么正确的断点方式应该是这个样子的。

    正确的断点方式 打印结果 此时就会有一个问题,这个init方法明显是被调用了俩次的,那么另一次调用的地方是在哪里呢!

    如果在不知道新的技巧之前,就会进行一系列的断点打印,看在哪里进行了执行,比如在这个init的上层去打印。

    也就是在initialize那个方法里边去打印做断点,但是这样很是麻烦的,而且很有可能浪费了大量的时间还是找不到正确的地方。

    小技巧之debug_backtrace()

    这个方法会产生一条回溯追踪,会显示出一个方法所有的调用位置。

    使用方式就是如下图,只需要把debug_backtrace这个方法打印出来即可。

    使用方法 打印结果1 打印结果2

    根据得到的数据信息,就可以非常快的进行定位。

    第一次就是在app类的215行。

    第一次调用init的地方
    第一次调用init的地方

    第二次是在thinkphp/library/think/route/dispatch/Module.php类的60行

    第二次调用地方
    第二次调用地方

    可以在这里做一个打印,看一下这个module是否为index

    断点 打印结果 所以说有了这个方法就可以非常快速地定位调用位置。

    三、框架执行流程之初始化应用init分析

    上文给大家提供了一个小技巧debug_backtrace实战演示了如何查看一个方法都在哪里执行的。

    并且案例也是使用的init这个方法来演示的,因为接下来就是要对init这个方法进行深入的了解。

    在init方法里边主要做的事情在上边的脑图已经描述的很清楚了。

    • 从一开始就对模块的定位,就是在第二节中的对init方法的调用,会传入对应的模块
    • 加载app目录下的tags文件,在tags文件里边就是对行为扩展定义的文件。在之前门面的文章中定义钩子执行就在这个文件中设置的。
    • 加载common文件,也就是公共文件,所以说公共文件就是在这里进行加载的。
    • 加载助手函数文件helper,在助手函数里边有一个大家特别熟悉的一个方法,那就是dump。这就是为什么在有的地方使用dump会报错的原因。
    • 加载中间件文件,这里的直接给出的是直接加载app目录下的中间件文件,但是在框架中我们需要在定义一个目录为http,在这个目录下定义中间件文件。
    • 注册服务的容器对象实例,这里注册就使用的是容器类中的bindTo方法进行绑定注册的。
    • 读取配置文件,这段在配置文件加载那一节中已经进行深入的说明了, 这里就不提了。配置文件会读取俩个地方一个是第一步模块下的config文件,另一个就是config目录下的配置文件。
    • 设置模块路径,会把第一步获取到的模块进行env环境变量配置里边
    • 最后一步就是对容器中的对象实例进行配置更新,具体更新了什么在后文中给大家详细说来。
        /**
         * 初始化应用或模块
         * @access public
         * @param  string $module 模块名
         * @return void
         */

        public function init($module = '')
        
    {
            // 定位模块目录
            $module = $module ? $module . DIRECTORY_SEPARATOR : '';
            /**
             * 第一次:D:phpstudy_proWWWThinkPHPSourceCodeAnalysisapplication
             * 第二次:D:phpstudy_proWWWThinkPHPSourceCodeAnalysisapplicationindex
             */

            $path   = $this->appPath . $module;

            // 加载初始化文件
            if (is_file($path . 'init.php')) {
                include $path . 'init.php';
            } elseif (is_file($this->runtimePath . $module . 'init.php')) {
                include $this->runtimePath . $module . 'init.php';
            } else {
                // 加载行为扩展文件
                if (is_file($path . 'tags.php')) {
                    $tags = include $path . 'tags.php';
                    if (is_array($tags)) {
                        $this->hook->import($tags);
                    }
                }

                // 加载公共文件
                if (is_file($path . 'common.php')) {
                    include_once $path . 'common.php';
                }

                if ('' == $module) {
                    // 加载系统助手函数
                    include $this->thinkPath . 'helper.php';
                }

                // 加载中间件
                if (is_file($path . 'middleware.php')) {
                    $middleware = include $path . 'middleware.php';
                    if (is_array($middleware)) {
                        $this->middleware->import($middleware);
                    }
                }

                // 注册服务的容器对象实例
                if (is_file($path . 'provider.php')) {
                    $provider = include $path . 'provider.php';
                    if (is_array($provider)) {
                        $this->bindTo($provider);
                    }
                }

                /**
                 * $path : "D:phpstudy_proWWWThinkPHPSourceCodeAnalysisapplication"
                 *          "D:phpstudy_proWWWThinkPHPSourceCodeAnalysisapplicationindex"
                 */

                // 自动读取配置文件
                if (is_dir($path . 'config')) {
                    $dir = $path . 'config' . DIRECTORY_SEPARATOR;
                } elseif (is_dir($this->configPath . $module)) {
                    // D:phpstudy_proWWWThinkPHPSourceCodeAnalysisconfig
                    $dir = $this->configPath . $module;
                }
                // scandir:以升序的方式读取目录中的文件
                // 返回就是config目录中的所有文件
                $files = isset($dir) ? scandir($dir) : [];

                foreach ($files as $file) {
                    /**
                     * $this->configExt:配置文件的后缀
                     * pathinfo返回的是文件后缀,关于pathinfo共有三个可选的参数PATHINFO_DIRNAME、PATHINFO_BASENAME、PATHINFO_EXTENSION,分别为只返回文件名,文件目录名,文件扩展
                     */

                    if ('.' . pathinfo($file, PATHINFO_EXTENSION) === $this->configExt) {
                        /**
                         * 俩个参数分别为
                         * 1.目录+config目录下的文件
                         * 2.config目录下文件名
                         */

                        $this->config->load($dir . $file, pathinfo($file, PATHINFO_FILENAME));
                    }
                }
            }

            $this->setModulePath($path);

            if ($module) {
                // 对容器中的对象实例进行配置更新
                $this->containerConfigUpdate($module);
            }
        }

    这里附带上一份代码,可以对着代码看上边的执行流程,对每一步都做了简单的说明。

    咔咔个人见解对源码进行优化

    在设置模块的这步代码咔咔感觉不是很是严谨,因为init方法会在俩个地方进行执行。

    第一次的模块为空,这块代码执行是没有任何意义的。

    下面在对容器的对象实例进行配置更新时进行了一次判断,判断模块的这个参数是否为空,如果不为空才会执行。

    那么同样的道理,咔咔感觉在设置模块路径这块也应该在这个判断里边。

    虽说第二次执行会把第一次的结果覆盖掉,但是咔咔感觉下图这样使用才会更好。

    咔咔对源码修改建议
    咔咔对源码修改建议

    四、对容器中的对象实例进行更新配置

    在上一节中这里就是最后的内容,那这个对实例进行更新配置,到底更新了什么,怎么更新没有说明。

    在这一小节中就会做出说明,同样可以配合着前言的思维导图看。

    • 先会把config目录下的所有配置信息全部获取出来
    • 从app配置文件中将注册异常处理类
    • 第三大块是把第一步获取出来的所有配置信息给对应的类进行注册配置。
    • 第四步就是在把模块确定下来之后加载对应的语言包,语言包功能就可以实现多语言功能,之前咔咔写过一篇文章实现多语言功能,如果感兴趣的可以去查看。
    • 最后一步就是根据app配置文件中的三个属性进行缓存的处理

    在这一节中咔咔感觉最重要的就是下图的内容了。

    对容器中的对象实例进行更新配置 我们可以随意追踪一到俩个方法查看一下那边到底执行了什么方法。

    追踪方法Db::init()

    追踪方法过来后可以看到就是对Db类中的config属性进行赋值,把database中的值赋值给Db类中的config属性。

    追踪Db::init方法 追踪方法$this->middleware->setConfig()

    来到中间件这个类里边,可以看到就是把本类的配置和传递过来的参数类进行合并,同样也是进行config属性的赋值。

    跟上边案例的Db类的init方法实现的效果是一致的。

    这里在提一嘴就是在对容器中的对象实例进行更新配置这一幅图中可以看到紫色部分是在本类中没有引用的。

    那么这是怎么可以进行执行的呢!是因为App类继承了容器类,容器类中有四个魔术方法,其中有一个__get方法,就是在获取不存在的属性时会执行那个方法。

    在魔术方法__get方法中执行了一个make方法,这个make方法说了好多次了,这个方法最终会返回一个应用的实例,然后用这个实例调用对应实例类的方法。

    这一块一定要理解好,阅读源码就是这个样子,我们需要对一切未知的进行的解决,只有这样才能提高我们的编程能力和思想。

    中间件的设置配置
    中间件的设置配置

    五、浅谈调试模式以及代码冗余

    本节会对调试模式做出简单的说明,并且会对框架代码冗余情况进行简单的提出。

    没有人写的代码是没有漏洞的,如果有那就是你还没有达到一定的造诣。

    调试模式

    在第一节中只提到了initialize方法的上半部分,因为在这一节之前聊的都是关于应用初始化init的内容。

    接下来会对这一块的内容进行简单的说明。

    • 从app配置文件中获取到app_debug的配置项
    • 给环境变量设置debug级别
    • 当框架中的debug是关闭状态时会执行ini_set这个方法,这个方法是为一个配置选项进行赋值。

    接下来的内容估计不是很好理解,都是平时在工作中根本使用不到的。

    • ob_get_level:返回输出缓冲机制的嵌套级别,那么怎么去理解呢!其实就是当缓存区不起作用时会返回0。
    • ob_get_clean:这个函数将会返回输出缓冲的内容并终止输出缓冲。如果缓冲区没有有效内容则返回false。本质上相当于同时执行了ob_getcontens()和ob_end_clean()。
    • ob_start:打开输出控制缓冲

    上边这三个先暂时认识就行,后期如果有机会会专门出一篇文章做解释的。

    调试模式 关于框架代码冗余

    这里也仅仅代表咔咔个人的观点。

    可以先看看这部分的代码,这俩处代码是不是很是熟悉,没错就是在上文的init方法中容器对象实例配置更新见到过。

    冗余代码 如图

    冗余代码对比 这块也就是咔咔个人提出的见解,由于咔咔式针对5.1做的源码解读,不太了解新版版是否做出了改动。

    六、总结

    本节主要是针对框架执行流程中的初始化应用做了简单的探讨。

    至于在app类的run方法下面还有很多的执行过程在这一节中没有做过多的解释。

    在阅读源码的过程中给大家提了一个很好得小技巧,那就是如何去查看一个方法都在哪里进行了执行。

    这个方法为debug_backtrace,这个方法需要大家多使用几次就知道怎么使用了,因为在打印出来的结果中也存在很多无用的信息。

    这个方法在调试源码的过程中是非常有效的,一定要好好利用这个方法。

    在就是对初始化应用init方法进行了特别详细的介绍。

    其中咔咔感觉这块设计最好的就是在容器中的对象实例进行更新配置那一块,先读取所有的配置,然后在通过各个类的方法进行配置的设置。

    这种代码规划和设计思路值得我们去学习。

    最后聊到了调试模式和框架的代码冗余问题,关于调试模式这里咔咔给大家提个醒项目在线上的调试模式一定要关闭。

    否则你的项目就类似于裸奔的存在,没有一点点的安全可言。

    这块有点不好理解的就是对于缓冲区,关于这块的内容咔咔认为暂时没有必要去钻牛角尖,先认识认识然后在进行深入的研究。

    缓冲区的这块内容估计工作了三四年的也很少有人使用,所以先认识,知道怎么一回事,咔咔后期学习了之后在给大家进行补充。

    直到这里关于框架的执行流程之初始化应用就结束了,这一节没有过深需要学习的,主要是其中的代码设计模式和实现思路。

    最后这个图大家一定要跟着源码看一看哈!

    在这里插入图片描述
    在这里插入图片描述

    坚持学习、坚持写博、坚持分享是咔咔从业以来一直所秉持的信念。希望在偌大互联网中咔咔的文章能带给你一丝丝帮助。我是咔咔,下期见。

  • 相关阅读:
    TensorFlow实现LeNet5模型
    jmeter+influxdb+grafana性能测试可视化报告
    jmeter命令执行脚本
    jmeter JSON Extractor使用
    jmeter上传文件
    jenkins登录信息无效,忘记密码
    XML
    JMeter函数和变量
    jmeter配置CSV Data Set Config
    jmeter发送Query String Parameters格式参数报错
  • 原文地址:https://www.cnblogs.com/fkaka/p/13947181.html
Copyright © 2020-2023  润新知