method_exists vs. is_callable

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.



Tags:

Eintrag von Stephan Hochdörfer am 27.01.2017

comments powered by Disqus