首页  >  问答  >  正文

当 PHP 中不可能时 require 方法返回 int

<p>我有以下代码,它将一些php代码保存到文件中,然后加载它,再次运行,有时require方法返回int,为什么会发生这种情况?</p> <h1>演示.php</h1> <pre class="brush:php;toolbar:false;"><?php $f = function() use($a){ $cachePath = '/tmp/t.php'; $code = '<?php'; $code .= "\n\n"; $code .= 'return ' . var_export([], true) . ';'; file_put_contents($cachePath, $code, LOCK_EX); if (file_exists($cachePath)) { // Sometime the following line returns int,why? $result = require($cachePath); if (!is_array($result)) { var_dump($result, $cachePath, file_get_contents($cachePath)); exit("ok"); } var_dump($result); } }; for($i=0;$i<1000000;$i++) { $f(); }</pre> <h1>如何重现?</h1> <p>使用两个 php 进程运行上面的代码</p> <pre class="brush:php;toolbar:false;">php demo.php</pre></p>
P粉883223328P粉883223328434 天前576

全部回复(2)我来回复

  • P粉351138462

    P粉3511384622023-09-03 09:03:54

    这是 require 的标准行为,与 include 的文档,这两者之间的行为是相同的:

    如您所见,当返回值未被覆盖时,快乐路径上会返回一个整数 (1)。

    这对于您的示例来说是有意义的,到目前为止,文件存在(因此没有致命错误),但由于文件刚刚创建,因此它可能只是被截断,也就是说,它是空的。

    因此返回值不会被覆盖,您可以看到 int(1)。

    另一种解释自然是您已经用整数覆盖,这也是可能的,因为多个进程可以写入同一个文件,但对于您编写示例的方式来说,这种可能性较小。我只是提到它,因为这是另一个有效的解释。

    如果存在则包含

    示例如何在您查找 $result 时悬浮竞争条件,而不是(仅)在文件存在时:

    if (($result = @include($cachePath)) &&
        is_array($result)    
    ) {
       # $result is array, which is required
       # ...
    }
    

    其背后的想法是,我们只进行很少的错误处理,例如检查文件是否存在,否则无法包含该文件(include() 只会发出警告并以 $result = false 传递),然后如果 $result 加载确实适用于 is_array() 测试。

    这就是我们为错误而设计的,但我们知道我们在寻找什么,即 $result 是一个数组。

    这通常称为事务或事务操作。

    在这个新示例中,当 $result 数组为空时,我们甚至不会输入 if-body,例如不包含任何数据。

    在程序处理级别上,这可能是我们感兴趣的,文件存在或不存在、为空或不为空、甚至写错都是错误情况,它需要“吃掉”并使 $result 无效。

    定义错误不存在。

    处理解析错误(对于 Include-If-Exists)

    自 PHP 7.0 起,我们可以使用 include(),如果不幸的是返回的包含文件已写入一半,我们将看到 PHP 解析错误,该错误可以捕获

    # start the transaction
    $result = null;
    assert(
        is_string($cachePath) &&           # pathnames are strings,
        '' !== $cachePath &&               # never empty,
        false === strpos($cachePath, "rrreee") # and must not contain null-bytes
    );
    try {
        if (file_exists($cachePath)) {
            $result = include($cachePath);
        }
        # invalidate $result in case include() did technically work.
        if (!$result || !is_array($result) {
            $result = null;
        }
    } catch (Throwable $t) {
        # catch all errors and exceptions,
        # the fall-through is intended to invalidate $result.
        $result = null;
    } finally {
        # $result is not null, but a non-empty array if it worked.
        # $result is null, if it could not be acquired.
    }
    

    请参阅 PHP try-catch-finally 了解如何抛出异常/异常处理工作详细,assert()用于记录示例中输入参数$cachePath的含义。

    第二个示例不使用抑制操作“@”,原因是如果像前面的示例一样使用它,并且要包含的文件将包含真正的致命错误,则该致命错误将被静音。如今,在现代 PHP 中,这不再是一个大问题,但是使用 file_exists() + include() – 虽然由于检查时间与使用时间而存在竞争条件 – 对于不存在的文件是安全的(仅警告)并且致命错误不会被隐藏。

    正如您可能已经看到的,您了解的细节越多,就越难编写尽可能具有前瞻性的代码。我们决不能迷失在错误处理本身的错误处理中,而应该关注结果并定义这些错误不存在。

    也就是说,include() 仍然导致将数据加载到内存中,file_exists() 仅用于“抑制”警告,我们知道,尽管如此,include() 可能会发出警告并可能返回一个整数,而不是一个数组。


    现在,由于编程很困难:然后您可能会将其包装在一个循环中,例如重试三次。为什么不使用 for 循环 来计数并保护数字重试次数?

    回复
    0
  • P粉550323338

    P粉5503233382023-09-03 00:09:17

    如果脚本始终只有一个执行者,则此问题无法重现。

    如果您正在谈论并行运行此脚本,那么问题在于以独占模式写入文件并不能保护您稍后在写入过程中读取文件。

    进程可能正在写入文件(并拥有锁),但 require 不遵守该锁(文件系统锁是建议性的,而不是强制执行的)。

    所以正确的解决方案是:

    <?php
    
    $f = function()  use($a){
        $cachePath = '/tmp/t.php';
    
        /* Open the file for writing only. If the file does not exist, it is created.
           If it exists, it is neither truncated (as opposed to 'w'), nor the call to this function fails (as is the case with 'x').
           The file pointer is positioned on the beginning of the file. 
           This may be useful if it's desired to get an advisory lock (see flock()) before attempting to modify the file, as using 'w' could truncate the file before the lock was obtained (if truncation is desired, ftruncate() can be used after the lock is requested). */
        $fp = fopen($cachePath, "c");
    
        $code = '<?php';
        $code .= "\n\n";
        $code .= 'return ' . var_export([], true) . ';';
     
        // acquire exclusive lock, waits until lock is acquired
        flock($fp, LOCK_EX);
        // clear the file
        ftruncate($fp, 0);
        // write the contents
        fwrite($fp, $code);
    
        //wrong (see my answer as to why)
        //file_put_contents($cachePath, $code, LOCK_EX);
    
        //not needed
        //if (file_exists($cachePath)) {
            // Lock is held during require
            $result = require($cachePath);
            if (!is_array($result)) {
                var_dump($result, $cachePath, file_get_contents($cachePath));
                exit("ok");
            }
            var_dump($result);
        //}
        // closing the file implicitly releases the lock
        fclose($fp);
    };
    
    
    for($i=0;$i<1000000;$i++) {
        $f();
    }
    

    请注意,写入后不会释放并重新获取锁,因为另一个进程可能正在等待覆盖该文件。

    不发布它是为了确保编写的同一段代码也是 required。

    然而,这整件事从一开始就值得怀疑。

    为什么需要写入一个文件以便稍后require将其返回?

    回复
    0
  • 取消回复