I throw an exception like this:
public function findRole($role)
{
if(!is_string($role)){
throw new \InvalidArgumentException(
sprintf('Role should be a string, %s given.', gettype($role))
);
//...
}
I have seen some exceptions like this and would like to do the same:
error: json_decode() expects parameter 1 to be string, array given.
Any chance I can automatically throw an exception like this so that the exception automatically outputs the name of the function and the invalid argument number for me?
Those errors you want are printed automatically by PHP and probably handled nicely with a set_error_handler function. There's no way you can simulate the same behavior yourself (possibly without nonsense hacks). Therefore you are forced to go with your exception way.
There's an exception that you should be aware of: type hinting; that can only be used with arrays, classes, objects and callables (functions):
public function acceptArray(array $array);
public function acceptObject(object $o);
public function acceptClass(MyClass $o);
public function acceptCallback(callable $f);
These functions if called with any other type of variable will complain almost like the specific error you posted.
The hacks I was talking about earlier might include redefining every type yourself:
class Int {...}
class String {...}
class Float {...}
class Bool {...}
and then use it like that:
$bool = new Bool(true);
acceptString($bool); // public function acceptString(String $s);
will trigger an error. But that's just not how PHP was supposed to work. Therefore I still suggest you to go with your initial idea.
Not automatically, but you could make kind of a generic template, for example like that:
if(!is_string($role)) {
throw create_invalid_argument_exception(__METHOD__, 1, 'string', $role);
}
function create_invalid-argument_exception($method, $argNo, $expectedType, $actualValue) {
return new \InvalidArgumentException(
sprintf(
'%s expects parameter %d to be %s, %s given.',
$method, $argNo, $expectedType, gettype($actualValue)
)
);
}
For catching exceptions, you must use the construction:
try{
/** you code here */
}catch(Exception $e){
/** convert $e to json and output */
}
And wrap you main function by it
Related
I want to test that a method called foo() throws an exception. The problem is that I can't get PHPUnit expectException() to catch the exception.
foo() looks something like this:
public function foo()
{
$params = $this->readAndFormatConfig();
// exception actually gets thrown in this method
$this->method->throws->exception($params);
}
If I catch the exception manually it works fine, like this:
public function testFoo()
{
$badConfig = new Config([]);
$driver = new bar($badConfig);
$exceptionThrown = false;
try {
$driver->foo();
} catch (Exception $e) {
$exceptionThrown = true;
}
$this->assertTrue($exceptionThrown);
}
If I catch it using expectException, like this:
public function testFoo()
{
$badConfig = new Config([]);
$driver = new bar($badConfig);
$this->expectException(Exception::class);
$driver->foo();
}
the test fails and I get this exception:
MyTestClass::testFoo Invalid argument supplied for foreach()
The output of get_class($e) is PHPUnit_Framework_Error_Warning which surprised me, but explains why the first test works but the second doesn't.
I'd like to either ignore the warning and wait until a real exception is thrown, or get the original warning, not PHPUnit_Framework_Error_Warning.
I'm using php 5.6.32 and PHPUnit 5.7.15
Adding the following to boostrap.php converts warnings to exceptions.
function warningToException($errno, $errstr, $errfile, $errline)
{
throw new Exception($errstr . " on line " . $errline . " in file " . $errfile);
}
set_error_handler("warningToException", E_WARNING);
This allowed the following test pass.
public function testFoo()
{
$badConfig = new Config([]);
$driver = new bar($badConfig);
$this->expectException(Exception::class);
$driver->foo();
}
I think a better method would be to expect PHPUnit_Framework_Error_Warning in the test as ishegg suggested.
What I ended up actually doing was making sure the object I was passing to foreach was traversable before entering the loop.
public function foo()
{
if (is_array($x) || $x instanceof \Traversable) {
// do stuff
} else {
// return false;
}
}
I think it makes more sense to refactor the application code in this case. What I'm really trying to test is that the function foo returns false if the application is misconfigured, so handling the misconfiguration properly seems like the right path in my opinion.
If you stumble upon this question for some reason and really need to convert warnings to an exception class other than PHPUnit_Framework_Error_Warning this is how I did it.
Thanks to ishegg for pointing me in the right direction.
A function or method can be called dynamically using call_user_func_array. If the call itself fails, FALSE is returned. Also, call_user_func_array returns the return values from the function or method that is called.
So when the called function or method returns FALSE as well (for example, see SO example), that value would be recognised as a false positive.
How can one reliably check if the function or method call was executed succesfully by call_user_func_array?
EDIT: People tend to point out the existence of is_callable. But this is not about checking if a method exists before calling it, thus avoiding possible errors. Actually before doing call_user_func_array the function call and it's arguments and argument types are already verified using Reflection to avoid a Massive Assign attack.
The documentation mentions the FALSE return value, but I fail to see how it can be used to check if the call was succesful.
You can explicitly check whether an error occurred during the last call:
error_clear_last(); // since PHP 7, before that you'll need to store and
// compare the error state before and after the call
$result = call_user_func_array($foo, $bar);
if ($result === false && error_get_last()) {
echo 'Failed to call ', $foo;
}
The above is a generic check for any error, perhaps you want to inspect the last error in more detail. It'll look something like:
Array
(
[type] => 2
[message] => call_user_func_array() expects parameter 1 to be a valid callback, function 'foo' not found or invalid function name
[file] => /in/M8PrG
[line] => 3
)
You might want to check that the message matches something like 'call_user_func_array() expects parameter 1 to be a valid callback' and/or that the line it refers to is the line above. Note that especially checking the message may break between PHP versions.
The alternative is to check before whether your supposed callback is_callable.
I'd transform the boolean callable into one that is void but that throws an exception on error.
That way, you could catch the exception and you would know if false was returned by the call_user_func_array that only its call failed:
<?php
$booleanCallable = function (... $args): bool {
foreach ($args as $arg) {
echo "$arg \n";
};
return false;
};
$transformBooleanCallableToVoidThrowingException = function (callable $c): callable {
return function (... $args) use ($c): void {
if (false === $c(... $args)) {
throw new \RuntimeException("the call to the callable failed");
}
};
};
try {
$callable = $transformBooleanCallableToVoidThrowingException($booleanCallable);
$response = call_user_func_array($callable, [1, 2, 3]);
if (false === $response) {
throw new \RuntimeException("call_user_func_array failed");
}
} catch (\Exception $e) {
echo $e->getMessage();
}
This will output the provided arguments and an error message:
1
2
3
the call to the callable failed
In the PHP project I'm working on there are several methods that make use of individual try/catch blocks. Here is one example:
public function getListData()
{
$clauses['filter'] = '';
$clauses['sort'] = 'CAST(propertyID AS INT) DESC';
try
{
return $this->getModel()->getListData($clauses);
}
catch (Exception $e)
{
// create an Error() object, send $e->getMessage() to it
}
}
Now, keeping in mind there are several similar methods, it would seem more efficient to write a method in the Model class that would look like this:
public function run($method)
{
try
{
return $this->$method;
}
catch (Exception $e)
{
//create an Error() object, send $e->getMessage() to it
}
}
The problem is calling it. This does not work:
public function getListData()
{
return $this->getModel()->run('getListData($clauses)');
}
The error is:
Undefined property:
classes\utility\Model::$getListData($clauses).
Is there a way to get this to work?
I'm going to assume that the first and second getListData() methods are in separate classes, otherwise you are calling a loop, since getListData would call getListData...which would call, you get it.
However, the way you are calling the method is incorrect in the run() method. It should be called using call_user_func. It is a callback to the method, not a call to the property, of the class.
You could call it statically using
public function run($method, $data)
{
try
{
return call_user_func(array($this, $method), $data);
}
catch (Exception $e)
{
//create an Error() object, send $e->getMessage() to it
}
}
public function getListData()
{
return $this->getModel()->run('getListData', $clauses);
}
There are several problems with this approach:
It prevents you from listening for custom exceptions
You can throw exceptions other than Exception, but this type of wrapper will make it much more difficult to do so.
It is difficult to follow the execution flow
When you pass method names and parameters around as strings, it becomes much harder for humans, IDEs and code analysis tools to understand what the program will do at runtime.
Try/catch blocks are cheap
The code required to catch exceptions is very simple and easy to use. This wrapper adds more complexity and more cost (an extra function call).
Consider just using try/catch blocks where needed instead of using the wrapper. If you have fifty similar methods as described in your comment above, you may gain more efficiency by eliminating the duplicate business logic and combining those methods.
You could simply convert errors to exceptions using this code:
set_error_handler(function ($errno, $errstr, $errfile, $errline)
{
if ((error_reporting() & $errno) === $errno)
throw new \Exception("$errstr ($errfile: $errline)", (int) $errno);
}, -1);
After it any error would be converted to exception.
Lets assume I have a class called Form. This class uses the magic method __call() to add fields to itself like so:
<?php
class Form {
private $_fields = array();
public function __call($name, $args) {
// Only allow methods which begin with 'add'
if ( preg_match('/^add/', $name) ) {
// Add a new field
} else {
// PHP throw the default 'undefined method' error
}
}
}
My problem is that I can't figure out how to make PHP handle the calls to undefined methods in it's default way. Of course, the default behavior can be mimicked in many ways, for example I use the following code right now:
trigger_error('Call to undefined method ' . __CLASS__ . '::' . $function, E_USER_ERROR);
But I don't like this solution because the error itself or its level might change in the future, so is there a better way to handle this in PHP?
Update
Seems like my question is a little vague, so to clarify more... How can I make PHP throw the default error for undefined methods without the need to supply the error and it's level? The following code won't work in PHP, but it's what I'm trying to do:
// This won't work because my class is not a subclass. If it were, the parent would have
// handled the error
parent::__call($name, $args);
// or is there a PHP function like...
trigger_default_error(E_Undefined_Method);
If anyone is familiar with ruby, this can be achieved by calling the super method inside method_missing. How can I replicate that in PHP?
Use exceptions, that's what they're for
public function __call($name, $args) {
// Only allow methods which begin with 'add'
if ( preg_match('/^add/', $name) ) {
// Add a new field
} else {
throw new BadMethodCallException('Call to undefined method ' . __CLASS__ . '::' . $name);
}
}
This is then trivially easy to catch
try {
$form->foo('bar');
} catch (BadMethodCallException $e) {
// exception caught here
$message = $e->getMessage();
}
If you want to change error level, just change it when necessary or add if statement instead of E_USER_ERROR
If I am correct, SimpleTest will allow you to assert a PHP error is thrown. However, I can't figure out how to use it, based on the documentation. I want to assert that the object I pass into my constructor is an instance of MyOtherObject
class Object {
public function __construct(MyOtherObject $object) {
//do something with $object
}
}
//...and in my test I have...
public function testConstruct_ExpectsAnInstanceOfMyOtherObject() {
$notAnObject = 'foobar';
$object = new Object($notAnObject);
$this->expectError($object);
}
Where am I going wrong?
Type hinting throws E_RECOVERABLE_ERROR which can be caught by SimpleTest since PHP version 5.2. The following will catch any error containing the text "must be an instance of". The constructor of PatternExpectation takes a perl regex.
public function testConstruct_ExpectsAnInstanceOfMyOtherObject() {
$notAnObject = 'foobar';
$this->expectError(new PatternExpectation("/must be an instance of/i"));
$object = new Object($notAnObject);
}
PHP has both errors and exceptions, which work slightly different. Passing a wrong type to a typehinted function will raise an exception. You have to catch that in your test case. Eg.:
public function testConstruct_ExpectsAnInstanceOfMyOtherObject() {
$notAnObject = 'foobar';
try {
$object = new Object($notAnObject);
$this->fail("Expected exception");
} catch (Exception $ex) {
$this->pass();
}
}
or simply:
public function testConstruct_ExpectsAnInstanceOfMyOtherObject() {
$this->expectException();
$notAnObject = 'foobar';
$object = new Object($notAnObject);
}
But note that this will halt the test after the line where the exception occurs.
Turns out, SimpleTest doesn't actually support this. You can't catch Fatal PHP errors in SimpleTest. Type hinting is great, except you can't test it. Type hinting throws fatal PHP errors.
you have to expect the error before it happens, then SimpleTest will swallow it and count a pass, if the test gets to the end and there is no error then it will fail. (there's expectError and expectException that act in the same way, for PHP (non-fatal) errors and Exceptions, respectively.)