method_exists vs. is_callable
As it turns out method_exists and is_callable work slightly different, and you might not be aware of it. I figured this out last year when I introduced protected methods in the config classes of bitexpert/disco. Recently I saw a similar issue in another open-source project and thought it might be a good idea to share my findings with the world.
Given this code:
class Demo
{
public function method1()
{
}
protected function method2()
{
}
}
method_exists() returns true for both methods:
$demo = new Demo;
var_dump(method_exists($demo, 'method1')); // bool(true)
var_dump(method_exists($demo, 'method2')); // bool(true)
The reason is obvious: As the PHP manual states method_exists() "checks if the class method exists". Since both method1 and method2 exist in the Demo class the method_exists() call does return true. It does not take the method visibility into account. Thus code like this can easily break with a PHP Fatal error "Uncaught Error: Call to protected method" when the code tries to call a non-public method like this:
function call_method(stdClass $object, string $method)
{
if (method_exists($object, $method)) {
return $object->$method();
}
throw \InvalidArgumentException();
}
If you want to know if the given method is callable from the current context - which is what you probably want in most cases - use the is_callable() function.
$demo = new Demo;
var_dump(is_callable([$demo, 'method1'])); // bool(true)
var_dump(is_callable([$demo, 'method2'])); // bool(false)
is_callable() keeps the current context in mind and thus is aware of the visibility of the method you want to call. But it also comes with a down side which you have to keep in mind: When you implement the __call() magic method in your class like this:
class Demo
{
public function method1()
{
}
protected function method2()
{
}
public function __call($name, $arguments)
{
}
}
is_callable() returns true for protected or private methods. Of course this makes sense as __call() gets invoked "when invoking inaccessible methods in an object context":
$demo = new Demo;
var_dump(is_callable([$demo, 'method1'])); // bool(true)
var_dump(is_callable([$demo, 'method2'])); // bool(true)
Usually this is not a problem but when trying to unit test your code - which you should - things can go south as a lot of the mocking frameworks use PHP's magic methods to track which methods were called.