I use set_error_handler() to set up an error handler within which I want to read the backtrace and get the name of a function where the error originated.
This works well for most built-in PHP functions which generate warnings. However when there is a count() function warning, the call entry of count() function doesn't appear in the backtrace at all. Instead the caller function foo() is immediately present in the backtrace. (Tested with PHP 7.4.)
Is there any reason for that and does any other built-in function behave like count()? (E.g. array_key_exists() behaves the same way.)
Playground: https://onlinephp.io/c/74421
<?php
set_error_handler(function () {
$backtrace = debug_backtrace();
var_dump($backtrace[1]["function"]);
return false;
});
function foo() {
// warning - "array_map(): Expected parameter 2 to be an array, null given"
// error handler dumps "array_map" -> GOOD
array_map("intval", null);
// warning - "count(): Parameter must be an array or an object that implements Countable"
// error handler dumps "foo" -> BAD (should dump "count")
count(null);
}
foo();
Related
I want to get an errors object from a callback function. here is what i have done so far.
$errors = null;
set_exception_handler(function($exception) use (&$errors) {
$errors = $exception;
var_dump($exception); // works fine
});
var_dump($errors); // give null ????
throw new Exception("blablblablablabla", 0);
I am sure with an anonymous function and a variable would work very fine !
But in this case with a callback and object I am not sure what's going on ? i can't get my head a round. any help explanation would be really appreciated.
set_exception_handler does not stop execution. It just defines what will happen if/when an exception occurs, so you can var_dump($errors); errors after setting it. $errors will still be null at that point because you have set it to null, and nothing has happened yet to change that.
The thing that does stop execution is when an exception actually occurs and the anonymous function defined in set_exception_handler is called. At that point, the var_dump($exception); within that function is called, but any other code in the main program after the point where the exception occurred will not be executed.
Storing the exception in a global variable from within the exception handler doesn't make really sense, because if the exception handler is called, no more code in the main program will be executed, so you won't be able to use that variable anyway.
You are assigning the anonymous function as an exception handler with set_exception_handler().
Yet you do not throw an exception in your code. Therefore the anonymous function is never called, because there was no event to handle, which means that $error was never set.
If you add a try catch block your function will also not be called. This can be seen by the fact, that the line var_dump($exception) from within your handler is not called either.
If you want to store the last called exception in your code, you may need to use a wrapper function around that code, that uses try catch to overwrite a static resource with the last exception encountered.
Note, that the exception within the given function is never fully thrown, because it was catched by the wrapper!
class Catcher {
private static $_EXCEPTION = null;
public static function GET_EXCEPTION () {
return self::$_EXCEPTION;
}
public static function run($func) {
try {
return $func();
} catch (Exception $e) {
self::$_EXCEPTION = $e;
}
}
}
var_dump(Catcher::GET_EXCEPTION()); //dumps NULL
Catcher::run(function(){
throw new Exception('I was called');
});
var_dump(Catcher::GET_EXCEPTION()); //dumps the last thrown exception
I want to use register_tick_function() to hook the following calls and print their stack trace with debug_backtrace().
If I run the following code.
<?php
function dump() {
// Replace this with var_dump(debug_backtrace()); to print the entire trace.
foreach (debug_backtrace() as $trace)
echo("Function ${trace['function']}() has been called" . PHP_EOL);
}
declare(ticks = 1);
register_tick_function('dump');
print("");
array_search('green', Array());
It prints only the dump() function.
Function dump() has been called
Function dump() has been called
Function dump() has been called
Why I'm not seeing print() and array_search() trace data? It's like the stack has been reset before invoking dump(). I'm also pretty sure it worked properly in the past.
You are misunderstanding what the backtrace is, this stack is not a list of functions that PHP executed before reaching a certain line of code, but rather the list of nested functions that justify PHP's interpreter to be at a specific line of code.
The first element will always be the function you are currently in, the next will be the function that called that function and so on. If a function returns, it is no longer in the stack.
class Foo {
public static function bar() {
return self::say();
}
public static function say() {
return debug_backtrace();
}
}
var_dump(Foo::say());
//[0] => say()
var_dump(Foo::bar());
//[0] => say();
//[1] => bar();
//Note that this behaviour work the same for require(), include() and eval()
require_once('test.php');
//test.php:
var_dump(debug_backtrace());
//[0] => require_once()
As PHP interprets your script, it navigates your functions, adding and removing from the current stack. The stack of the backtrace is not the place were PHP logs what functions were called in chronological order.
For the latter, this is a solution I found: Get a called functions list in PHP
Most of my recent experience is in Java, and I was quite surprised the following PHP code did not throw a compile-time error:
<?php
class Test { }
$a = new Test('foo', true, false, 18320);
Is there any way to force this to be a syntax or compile-time error?
PHP, being a very loosely-typed dynamic language, always allows you to pass any set and any number of arguments to a function or constructor call without error, unless type hints in their definitions aren't met by the arguments passed; for instance, this won't work:
class Test {
public function __construct(array $a, $b, $c, $d) {}
}
// Catchable fatal error: Argument 1 passed to Test::__construct()
// must be an array, string given
$a = new Test('foo', true, false, 18320);
But even then, that's not going to cause a syntax error either — that's a fatal error at runtime.
The closest you can get is to enforce the checks at runtime within the function or method bodies.
From memory, I don't think that would even throw a warning at runtime. The converse raises an error, thankfully! - supplying too few arguments.
There is a way to raise an error however: write a constructor which checks the argument count. You can use func_num_args() and func_get_args() for this. If you needed it several times, you could put it in a parent class and extend, though of course that would preclude any other inheritence hierarchy you may need.
Edit: a random inspiration made me do a search for "php lint". And guess what I found? Not tried it, though :)
As BoltClock said, you can't...
But if you really want to check this at runtime, you can explicitly declare a default constructor, taking no arguments, and use func_num_args to check the number of effectively passed arguments, raising an exception if applicable.
class Test
{
public function __construct()
{
if( func_num_args() > 0 )
{
/* Error management */
}
}
}
consider this code:
class TestClass {
function __construct() {
set_error_handler(array($this,'error_handler'));
}
function error_handler($errno,$errstr,$errfile,$errline,$errcontext) {
trigger_error("\ncode: [$errno]\nfile: [$errfile -> $errline]\nSTR: [$errstr]",E_USER_ERROR);
}
}
count($nonExistentVariable);
//notice was thrown above but script carry on
//but here error handler is registered and...
$anInstance = new TestClass();
// rest of the code is affected by it
count($nonExistentVariable); // user error is thrown
So is there a way to make the error_handler function to fire only when error is encountered inside an instance and do not mess globally with other code ?
You could check the call stack in your error handler (via debug_backtrace()) and determine if it's coming from within the class you're interested in. If it's not coming from the class be sure to return FALSE and PHP will fall back on it's default handler.
http://php.net/manual/en/function.set-error-handler.php
This question is specific to using PHPUnit.
PHPUnit automatically converts php errors to exceptions. Is there a way to test the return value of a method that happens to trigger a php error (either built-in errors or user generated errors via trigger_error)?
Example of code to test:
function load_file ($file)
{
if (! file_exists($file)) {
trigger_error("file {$file} does not exist", E_USER_WARNING);
return false;
}
return file_get_contents($file);
}
This is the type of test I want to write:
public function testLoadFile ()
{
$this->assertFalse(load_file('/some/non-existent/file'));
}
The problem I am having is that the triggered error causes my unit test to fail (as it should). But if I try to catch it, or set an expected exception any code that after the error is triggered never executes so I have no way of testing the return value of the method.
This example doesn't work:
public function testLoadFile ()
{
$this->setExpectedException('Exception');
$result = load_file('/some/non-existent/file');
// code after this point never gets executed
$this->assertFalse($result);
}
Any ideas how I could achieve this?
There is no way to do this within one unit test. It is possible if you break up testing the return value, and the notice into two different tests.
PHPUnit's error handler catches PHP errors and notices and converts them into Exceptions--which by definition stops program execution. The function you are testing never returns at all. You can, however, temporarily disable the conversion of errors into exceptions, even at runtime.
This is probably easier with an example, so, here's what the two tests should look like:
public function testLoadFileTriggersErrorWhenFileNotFound()
{
$this->setExpectedException('PHPUnit_Framework_Error_Warning'); // Or whichever exception it is
$result = load_file('/some/non-existent/file');
}
public function testLoadFileRetunsFalseWhenFileNotFound()
{
PHPUnit_Framework_Error_Warning::$enabled = FALSE;
$result = load_file('/some/non-existent/file');
$this->assertFalse($result);
}
This also has the added bonus of making your tests clearer, cleaner and self documenting.
Re: Comment:
That's a great question, and I had no idea until I ran a couple of tests. It looks as if it will not restore the default/original value, at least as of PHPUnit 3.3.17 (the current stable release right now).
So, I would actually amend the above to look like so:
public function testLoadFileRetunsFalseWhenFileNotFound()
{
$warningEnabledOrig = PHPUnit_Framework_Error_Warning::$enabled;
PHPUnit_Framework_Error_Warning::$enabled = false;
$result = load_file('/some/non-existent/file');
$this->assertFalse($result);
PHPUnit_Framework_Error_Warning::$enabled = $warningEnabledOrig;
}
Re: Second Comment:
That's not completely true. I'm looking at PHPUnit's error handler, and it works as follows:
If it is an E_WARNING, use PHPUnit_Framework_Error_Warning as an exception class.
If it is an E_NOTICE or E_STRICT error, use PHPUnit_Framework_Error_Notice
Else, use PHPUnit_Framework_Error as the exception class.
So, yes, errors of the E_USER_* are not turned into PHPUnit's *_Warning or *_Notice class, they are still transformed into a generic PHPUnit_Framework_Error exception.
Further Thoughts
While it depends exactly on how the function is used, I'd probably switch to throwing an actual exception instead of triggering an error, if it were me. Yes, this would change the logic flow of the method, and the code that uses the method... right now the execution does not stop when it cannot read a file. But that's up to you to decide whether the requested file not existing is truly exceptional behaviour. I tend to use exceptions way more than errors/warnings/notices, because they are easier to handle, test and work into your application flow. I usually reserve the notices for things like depreciated method calls, etc.
Use a phpunit.xml configuration file and disable the notice/warning/error to Exception conversion. More details in the manual. It's basically something like this:
<phpunit convertErrorsToExceptions="false"
convertNoticesToExceptions="false"
convertWarningsToExceptions="false">
</phpunit>
Instead of expecting a generic "Exception", what about expecting a "PHPUnit_Framework_Error" ?
Something like this might do :
/**
* #expectedException PHPUnit_Framework_Error
*/
public function testFailingInclude()
{
include 'not_existing_file.php';
}
Which, I suppose, might also be written as :
public function testLoadFile ()
{
$this->setExpectedException('PHPUnit_Framework_Error');
$result = load_file('/some/non-existent/file');
// code after this point never gets executed
$this->assertFalse($result);
}
For more informations, see Testing PHP Errors
Especially, it says (quoting) :
PHPUnit_Framework_Error_Notice and
PHPUnit_Framework_Error_Warning represent
PHP notices and warning, respectively.
Looking at the /usr/share/php/PHPUnit/TextUI/TestRunner.php file I have on my system, I see this (line 198 and following) :
if (!$arguments['convertNoticesToExceptions']) {
PHPUnit_Framework_Error_Notice::$enabled = FALSE;
}
if (!$arguments['convertWarningsToExceptions']) {
PHPUnit_Framework_Error_Warning::$enabled = FALSE;
}
So maybe you'll have to pass some kind of parameter to activate that behaviour ? But it seems to be enabled by default...
Actually there is a way to test both the return value and the exception thrown (in this case an error converted by PHPUnit).
You just have to do the following:
public function testLoadFileTriggersErrorWhenFileNotFound()
{
$this->assertFalse(#load_file('/some/non-existent/file'));
$this->setExpectedException('PHPUnit_Framework_Error_Warning'); // Or whichever exception it is
load_file('/some/non-existent/file');
}
Notice that to test for the return value you have to use the error suppression operator on the function call (the # before the function name). This way no exception will be thrown and the execution will continue. You then have to set the expected exception as usual to test the error.
What you cannot do is test multiple exceptions within a unit test.
This answer is a bit late to the party, but anyhow:
You can use Netsilik/BaseTestCase (MIT License) to test directly for triggered Notices/Warnings, without ignoring them or converting them to Exceptions. Because the notices/warnings they are not converted to an Exception, the execution is not halted.
composer require netsilik/base-test-case
Testing for an E_USER_NOTICE:
<?php
namespace Tests;
class MyTestCase extends \Netsilik\Testing\BaseTestCase
{
public function test_whenNoticeTriggered_weCanTestForIt()
{
$foo = new Foo();
$foo->bar();
self::assertErrorTriggered(E_USER_NOTICE, 'The notice message');
}
}
Hope this helps someone in the future.