search

Home  >  Q&A  >  body text

require method returns int when not possible in PHP

<p>I have the following code which saves some php code to a file and then loads it and runs it again and sometimes the require method returns int, why does this happen? </p> <h1>demo.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>How to reproduce? </h1> <p>Use two php processes to run the above code</p> <pre class="brush:php;toolbar:false;">php demo.php</pre></p>
P粉883223328P粉883223328525 days ago615

reply all(2)I'll reply

  • P粉351138462

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

    This is the standard behavior of require, and the behavior of include is the same:

    As you can see, when the return value is not overwritten, an integer (1) is returned on the happy path.

    This makes sense for your example, so far the file exists (so no fatal error), but since the file has just been created, it might just be truncated, i.e., it's empty.

    So the return value is not overwritten and you can see the int(1).

    Another explanation is naturally that you have overwritten with an integer, which is also possible since multiple processes can write to the same file, but with the way you wrote your example it is less likely. I only mention it because it's another valid explanation.

    Contains if present

    Example how to suspend a race condition when you look for $result, instead of (only) when the file exists:

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

    The idea behind it is that we do very little error handling, like checking if the file exists, otherwise it can't be included (include() just emits a warning and passes it with $result = false), and then if $result Loading does work with is_array() tests.

    This is what we have for errors, but we know what we are looking for, i.e. $result is an array.

    This is often called a transaction or transaction operation.

    In this new example, we don't even enter the if-body when the $result array is empty, i.e. does not contain any data.

    On a program processing level this might be of interest to us, file presence or absence, being empty or not empty, or even being written incorrectly are all error conditions that need to be "eaten" and invalidate the $result.

    The definition error does not exist.

    Handling parsing errors (for Include-If-Exists)

    Since PHP 7.0 we can use include() and if unfortunately the returned include file is half written we will see a PHP parsing error which can be caught :

    # 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.
    }
    

    Please refer to PHP try-catch-finally to learn how to throw exceptions/exception handling work details, assert() is used to record the meaning of the input parameter $cachePath in the example.

    The second example does not use the suppress operation "@", the reason is that if it were used like the previous example, and the file to be included would contain a real fatal error, the fatal error would be silenced. Nowadays, in modern PHP, this is not a big problem anymore, but using file_exists() include() - although there is a race condition due to check time vs. use time - is safe (only a warning) and fatal for non-existent files Errors will not be hidden.

    As you may have seen, the more details you know, the harder it is to write code that is as future-proof as possible. We must not get lost in error handling itself, but should focus on the results and define that these errors do not exist.

    That said, include() still causes the data to be loaded into memory, file_exists() is only used to "suppress" the warning, we know that, nevertheless, include() may issue a warning and may return an integer, while Not an array.


    Now, since programming is hard: you might then wrap it in a loop and retry three times, for example. Why not use a for loop to count and protect the number of retries?

    reply
    0
  • P粉550323338

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

    If the script always has only one executor, this problem cannot be reproduced.

    If you are talking about running this script in parallel, the problem is that writing the file in exclusive mode does not protect you from reading the file later during the writing process.

    A process may be writing to the file (and own the lock), but require does not honor the lock (filesystem locks are advisory, not enforced).

    So the correct solution is:

    <?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();
    }
    

    Note that the lock is not released and reacquired after writing, as another process may be waiting to overwrite the file.

    Not releasing it is to ensure that the same code written is also required.

    However, this whole thing was questionable from the beginning.

    Why do I need to write to a file so that it can be returned later require?

    reply
    0
  • Cancelreply