PHP 文件引入细节

2020/10/20 PHP

今天在 workerman 的官方群看到有人问 webman 中的一个问题。大意就是 webman 在 windows 中修改 controller 代码需要重启才生效,而修改 view 代码则不需要重启也能生效(注:webman 使用了作者自己写的 FileMonitor 组件。在 Linux 系统中,可以通过监控指定目录,定时更新文件,然后通过 kill 给子进程发送信号达到重新加载的目的;而 windows 系统并没有相应的扩展支持)。这是为什么呢?

感觉这个问题有点意思,但自己又从来没有玩过 webman,于是运行之后稍微看了下 controller 调用 view 的逻辑,很快顺藤摸瓜找到了其中的调用路径app/controller/Index:viewsuppport/helpers:viewsupport/view/Raw|ThinkPHP...:render,最后将问题关键锁定在include $view_path上,如下:

<?php
namespace support\view;

use Webman\View;

/**
 * Class Raw
 * @package support\view
 */
class Raw implements View
{
    public static function render($template, $vars, $app = null)
    {
        static $view_suffix;
        $view_suffix = $view_suffix ? : config('view.view_suffix', 'html');
        $app_name = $app == null ? request()->app : $app;
        if ($app_name === '') {
            $view_path = app_path() . "/view/$template.$view_suffix";
        } else {
            $view_path = app_path() . "/$app_name/view/$template.$view_suffix";
        }
        \extract($vars);
        \ob_start();
        // Try to include php file.
        try {
            include $view_path;
        } catch (\Throwable $e) {
            echo $e;
        }
        return \ob_get_clean();
    }
}

其实 include、require、include_once、require_once 这几个是面试中的常客了,而且日常使用非常频繁,但我猜很多人对其认识应该也仅仅限于引入发生错误的等级不一样有 _once 的在多次引入时只引入一次之类的。

再回到 webman 中,上面提到问题的关键在于include $view_path。我们知道 PHP 想要调用其他文件的类、函数时,都是需要引入文件的,因此,webman 的控制器肯定也会用到的,那么控制器的引入和视图的引入区别在哪呢?webman 的控制器通过 require_once 引入。如下(由于并未使用过 webman,不确定是不是在这里,看起来像):

<?php
namespace Webman;

/**
 * Class App
 * @package Webman
 */
class App
{
    /**
     * @return void
     */
    public static function loadController($path)
    {
        if (\strpos($path, 'phar://') === false) {
            foreach (\glob($path . '/controller/*.php') as $file) {
                require_once $file;
            }
            foreach (\glob($path . '/*/controller/*.php') as $file) {
                require_once $file;
            }
        } else {
            $dir_iterator = new \RecursiveDirectoryIterator($path);
            $iterator = new \RecursiveIteratorIterator($dir_iterator);
            foreach ($iterator as $file) {
                if (is_dir($file)) {
                    continue;
                }
                $fileinfo = new \SplFileInfo($file);
                $ext = $fileinfo->getExtension();
                if (\strpos($file, '/controller/') !== false && $ext === 'php') {
                    require_once $file;
                }
            }
        }
    }
}

一般情况下,大部分人都使用 php-fpm 模式,而几种引入文件的方式在该模式下其实区别并没有那么大,随便选一种问题都不大。但是在 cli 模式下常驻内存时,这几种引入文件的方式区别可就大了。可以使用以下代码来体验一下:

<?php
// include_require.php

$ld = stream_socket_server('tcp://0.0.0.0:10003', $errno, $errstr, STREAM_SERVER_BIND|STREAM_SERVER_LISTEN);
if (!$ld) {
    printf("socket create fail\n");
    exit(-1);
}

while (true) {
    echo time(), "\n";
    $conn = stream_socket_accept($ld);
    if ($conn) {
        extract(['name' => 'william']);
        ob_start();
        // TODO: 分别注释以下两行代码,或者将 require 改成 include
        require "include_require.html";
//        require_once "include_require.html";
        $body = ob_get_clean();

        $header = <<<HEADER
HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n
HEADER;

        $sendRes = stream_socket_sendto($conn, $header);
        $sendRes = stream_socket_sendto($conn, $body);
        if (!$sendRes) {
            printf("send fail\n");
        }

        fclose($conn);
    } else {
        printf("accept fail\n");
    }

    echo time(), "\n";
}

fclose($ld);
<!-- include_require.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<?php
    echo $name;
?>
</body>
</html>

关注点放在以上代码的TODO处,分别用四种不同的方式引入文件,并在运行过程中修改 html 文件对比一下。可以用strace php include_require.php命令对比几种引入方式的实质区别。以下是命令在 include 或 require 时输出的片段:

lstat("/usr/share/php/notes/src/github.com/lwlwilliam/practice/php/common/./include_require.html", {st_mode=S_IFREG|0777, st_size=158, ...}) = 0
lstat("/usr/share/php/notes/src/github.com/lwlwilliam/practice/php/common/include_require.html", {st_mode=S_IFREG|0777, st_size=158, ...}) = 0
openat(AT_FDCWD, "/usr/share/php/notes/src/github.com/lwlwilliam/practice/php/common/include_require.html", O_RDONLY) = 5
fstat(5, {st_mode=S_IFREG|0777, st_size=158, ...}) = 0
read(5, "<!DOCTYPE html>\r\n<html lang=\"en\""..., 158) = 158
close(5)                                = 0

经过比较,发现 include 和 require 的系统调用完全一致,而 include_once 和 require_once 则在第一次引入时的系统调用亦一致,区别就在于第一次之后的引入则完全被忽略。可以看出,引入文件的本质就是 open,read,close 这几个常见的读操作。

到这里,应该比较清晰地回答了文章开头的问题,由于 webman 框架在调用 view 时使用了 include,相当于每次都重新读取文件,因此,即使在 cli 模式常驻内存时,仍然能即时更新 view 内容。

Search

    Table of Contents