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.