diff --git a/src/Lib/Dispatcher.php b/src/Lib/Dispatcher.php index fe821a9..aba5772 100644 --- a/src/Lib/Dispatcher.php +++ b/src/Lib/Dispatcher.php @@ -138,7 +138,7 @@ public static function dispatch($requestUri, $isCli = false) } $result = null; - if (method_exists($ctrl, $action)) { + if (method_exists($ctrl, $action) || is_callable([$ctrl, $action])) { if ($router->isCallable($ctrl, $action)) { $result = $ctrl->$action(); } else { diff --git a/src/Lib/Router.php b/src/Lib/Router.php index 9444fad..98d8425 100644 --- a/src/Lib/Router.php +++ b/src/Lib/Router.php @@ -179,6 +179,7 @@ public static function filterURI($uri) /** * Uses PHP's ReflectionClass to test the given object for the given method's callability. * Only public, non-abstract, non-constructor/destructors are considered callable. + * Also supports objects that implement the magic methods __call or __callStatic. * * @param Object $obj The object we're searching * @param string $method The name of the method we're looking for in $obj @@ -195,8 +196,32 @@ public static function isCallable($obj, $method, $inheritsFrom = 'Controller') return false; } + $reflectedMethod = false; try { $reflectedMethod = $reflectedObj->getMethod($method); + } catch (ReflectionException $e) { + // The method does not exist on the object, but could still be available, e.g., via __call + } + + // If the method does not exist, but is still callable (e.g. via __call), then only ensure it meets + // the same criteria on inheritance and public uncallables + if ($reflectedMethod === false) { + try { + return ( + is_callable([$obj, $method]) + && !in_array(strtolower($method), $publicUncallables) + && ( + $inheritsFrom === null + || $reflectedObj->getName() == $inheritsFrom + || $reflectedObj->isSubclassOf($inheritsFrom) + ) + ); + } catch (Exception $e) { + return false; + } + } + + try { $declaredClass = $reflectedMethod->getDeclaringClass(); // A method may be required to inherit from the given class @@ -218,6 +243,8 @@ public static function isCallable($obj, $method, $inheritsFrom = 'Controller') return true; } catch (ReflectionException $e) { return false; + } catch (Exception $e) { + return false; } } diff --git a/tests/Unit/Lib/RouterTest.php b/tests/Unit/Lib/RouterTest.php index 5e70894..be0f03d 100644 --- a/tests/Unit/Lib/RouterTest.php +++ b/tests/Unit/Lib/RouterTest.php @@ -138,6 +138,31 @@ public function testIsCallable() $this->assertFalse(Router::isCallable($controller, 'preAction')); $this->assertFalse(Router::isCallable($controller, 'nonexistentMethod')); $this->assertFalse(Router::isCallable(null, null)); + + # + # TODO: add tests for __call + # NOTE: phpunit currently cannot handle mocking __call properly + # see https://github.com/sebastianbergmann/phpunit-mock-objects/issues/275 + # + /* + $magicController = $this->getMockBuilder('Controller') + ->disableOriginalConstructor() + ->setMethods(['__call']) + ->getMock(); + + $value = 'testAbc'; + $magicController->expects($this->any()) + ->method('__call') + ->willReturnCallback(function($name, $arguments) use ($value) { + return $value; + }); + + $this->assertTrue(Router::isCallable($magicController, 'index')); + $this->assertTrue(Router::isCallable($magicController, '__construct')); + $this->assertTrue(Router::isCallable($magicController, 'preAction')); + $this->assertTrue(Router::isCallable($magicController, 'nonexistentMethod')); + * + */ } /**