2

I have a custom error handler set up using set_error_handler. When I tried to include a file that doesn't exist, PHP calls error_handler one more time than it should:

<?php
error_reporting(E_ALL | E_STRICT);
set_error_handler(function($errno, $errstr, $errfile, $errline, $errcontext){
    if(error_reporting() !== 0){
        echo "<br>";
        echo "<br>In Custom Error Handler...";
        echo "<br>Err String: ", $errstr;
        echo "<br>Passing to Default Handler...";
    }
    return false; // allow default
});
include("/missing_file.php"); // line 11
?>

Output:

In Custom Error Handler... // this is the extra error handler call

Err String: include(/missing_file.php) [function.include]: failed to open stream: No such file or directory

Passing to Default Handler...

// the default handler does nothing, even though error_reporting is not zero

// Next Phase:

In Custom Error Handler...

Err String: include() [function.include]: Failed opening '/missing_file.php' for inclusion (include_path='.:/usr/lib/php:/usr/local/lib/php')

Passing to Default Handler...

Warning: include() [function.include]: Failed opening '/missing_file.php' for inclusion in /home/yccom/public_html/apr/test.php on line 11

The same behavior is observed with require.

For example, changing line 11 to require will give this output:

In Custom Error Handler... // this is the extra error handler call

Err String: require(/missing_file.php) [function.require]: failed to open stream: No such file or directory

Passing to Default Handler...

// the default handler does nothing, even though error_reporting is not zero

// Next Phase:

Fatal error: require() [function.require]: Failed opening required '/missing_file.php' in /home/yccom/public_html/apr/test.php on line 11

What may be causing the error handler's additional call?

Pacerier
  • 86,231
  • 106
  • 366
  • 634
  • Try to use `include_once()` to see if the same behaviour appears. – Arthur Jul 12 '13 at 14:26
  • Does the file `some_file.php` exist in the same directory as this file? – Tro Jul 12 '13 at 14:26
  • @Tro, no it doesn't exist anywhere. – Pacerier Jul 12 '13 at 14:26
  • @Arthur, include_once only gives 2 errors. The first line, and the last line. – Pacerier Jul 12 '13 at 14:28
  • tested your code, only got the last 2 errors, which is normal. – Flosculus Jul 12 '13 at 14:29
  • @Pacerier: Then your include might be executed 3 times (several includes, loops, etc.) – Arthur Jul 12 '13 at 14:30
  • @Flosculus, then what's the explanation for the 4 errors? – Pacerier Jul 12 '13 at 14:30
  • @Arthur, how so? The code above is the *complete* code. Which line is culprit, in particular? – Pacerier Jul 12 '13 at 14:31
  • @Pacerier: If it's really the complete code, I've no idea... Same errors as Flosculus here: http://workspace.hoa.ro/SO17617188/index.php – Arthur Jul 12 '13 at 14:37
  • @Arthur, even if it's 2 errors, how is 2 errors *normal*? The manual didn't say anything about emitting 2 errors. – Pacerier Jul 12 '13 at 14:40
  • @Pacerier One error for failed file stream, and another for failed include. Granted, the first error does cause the second error, but because `include` performs the tasks to `load the file` and `process the code`, both errors occurred as a result of the file not existing. – Flosculus Jul 12 '13 at 15:02
  • @Flosculus, since you said that this is *expected* behavior, wouldn't this kind of behavior be documented? – Pacerier Jul 12 '13 at 15:05
  • @Pacerier I've just dug in a bit further, the first error is failed to load the stream for the `file path` you specified, then it attempts to load the file from the `include paths`. Its basically telling you that it a) couldn't find the file at the requested path, and b) couldn't find it at any of the alternative include paths. – Flosculus Jul 12 '13 at 15:12
  • @Flosculus, but there is only one path in the include_path. This doesn't explain the 4 errors. Even if there are 10 paths in the include_path, it is still 4 errors. – Pacerier Jul 12 '13 at 15:17
  • @Pacerier I was only explaining the reason for the `2 errors` that I see, regarding the 4 errors, I would follow Elias, imo if it is not caused by a loop, and the code I tested is under the same conditions as the code you showed, I'd put my money on something in php.ini or version. – Flosculus Jul 12 '13 at 15:21
  • @Flosculus, I'm using 5.3.26, there's no loop, that's the complete page. My php.ini: http://goo.gl/Z6YKY – Pacerier Jul 12 '13 at 15:40

1 Answers1

2

It's quite simple, really. PHP's lifecycle consists of 4 distinct phases:

  1. Parsing
  2. Compilation
  3. Scanning
  4. Execution

for your code to be parsed, all files that are included/required need to be fetched in the first phase, to translate the code into meaningful expressions. Your file doesn't exist, so a warning is issued.
Next, the compilation phase encounters the same include statement, and tries to convert the expressions into opcodes. The file doesn't exist, so a warning is issued.
Scanning translates code into tokens, which again cannot be done for the missing include file.
Execution time... The file cannot be executed because it is missing.

Why would PHP work like this? Isn't it stupid to blunder along, eventhough a file is missing?
In a way, yes, but include is used to include files that are non-critical to the script, if you really need that file's contents, you use require (but preferably require_once). The latter emits, as you stated, a fatal error, and stops everything dead in its tracks. That's what should happen if you're depending on another file for your code to function.

The require construct issues an E_COMPILER_ERROR, which effectively halts the compiler (not unlike __halt_compiler) at a given offset(the line where the failing require statement resides).

Check these slides for more details on each of the 4 main phases.

The reason why your code emits four warnings, is simply because PHP tries to include the file four times. Try running the script from the command line, but use strace:

$ strace -o output.txt php yourScript.php

open the output file, and see the internals of the Zend engine. Pay special attention to lines that look like:

lstat("/your/path/./file.php", 0x50113add8355) = -1//0x5... ~= 0xsomeaddress

You'll see where PHP goes looking for the file: it's all its include_path directories, the cwd, /usr/share/php, probably a pear or lib directory, and the include path you explicitly set.
I've gotten the idea to do this from this site, and based on the output I got, this seems to me to be the most plausible explanation as to why you see multiple errors.

Elias Van Ootegem
  • 74,482
  • 9
  • 111
  • 149
  • Your answer implies that `require` would only throw a single error and exit. But even a `require` throws 4 errors (the first 3 being the same, and the fourth a fatal). Why does PHP blunder along even though files that are critical to the script are missing. – Pacerier Jul 12 '13 at 14:47
  • @Pacerier: I'm not _that_ familiar with the ZendEngine, but my guess would be that PHP, to generate the output (the fatal error) still has to go through all 4 steps, but I'm looking into that now – Elias Van Ootegem Jul 12 '13 at 14:54
  • @Pacerier: Try the same thing without the custom error handler, then you _should_ see a difference between include and require – Elias Van Ootegem Jul 12 '13 at 15:06
  • 1
    regarding your answer's last paragraph, that is simply not true. E_COMPILER_ERROR will not be handled with the custom error handler. see http://php.net/manual/en/function.set-error-handler.php. The only way E_COMPILER_ERROR can be handled is through register_shutdown_function http://stackoverflow.com/questions/277224/how-do-i-catch-a-php-fatal-error – Pacerier Jul 12 '13 at 15:07
  • 1
    @Pacerier: I've done some more digging, and I think I've found the reason why you get 4 errors instead of the expected 1. Editted my answer, and added a link that goes into more detail. +1 for your question, it's been fun getting back _"down and dirty"_ with the zend engine's internals – Elias Van Ootegem Jul 16 '13 at 08:22
  • thanks for the update. But I'm still not convinced this is the answer. For some reason no matter how many paths I have in my include_path, the number of error handler calls are the same. I'm still trying to dig more into it... – Pacerier Jul 16 '13 at 12:17
  • @Pacerier: Have you tried using system trace, to check what include paths are being used? – Elias Van Ootegem Jul 16 '13 at 12:22
  • I'm hosting on a virtual server and do not have access to system trace (unless you mean another thing). I've narrowed down the problem using error_reporting and absolute paths (thus offsetting include_path), take a look at the update. – Pacerier Jul 16 '13 at 13:16