diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index 4e804bf079701..10299a39b4fd5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -101,21 +101,25 @@ public function testGetControllerInvokableService() */ public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null) { - $this->setExpectedException($exceptionName, $exceptionMessage); + // All this logic needs to be duplicated, since calling parent::testGetControllerOnNonUndefinedFunction will override the expected excetion and not use the regex + $resolver = $this->createControllerResolver(); + $this->setExpectedExceptionRegExp($exceptionName, $exceptionMessage); - parent::testGetControllerOnNonUndefinedFunction($controller); + $request = Request::create('/'); + $request->attributes->set('_controller', $controller); + $resolver->getController($request); } public function getUndefinedControllers() { return array( - array('foo', '\LogicException', 'Unable to parse the controller name "foo".'), - array('foo::bar', '\InvalidArgumentException', 'Class "foo" does not exist.'), - array('stdClass', '\LogicException', 'Unable to parse the controller name "stdClass".'), + array('foo', '\LogicException', '/Unable to parse the controller name "foo"\./'), + array('foo::bar', '\InvalidArgumentException', '/Class "foo" does not exist\./'), + array('stdClass', '\LogicException', '/Unable to parse the controller name "stdClass"\./'), array( 'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar', '\InvalidArgumentException', - 'Controller "Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar" for URI "/" is not callable.', + '/.?[cC]ontroller(.*?) for URI "\/" is not callable\.( Expected method(.*) Available methods)?/', ), ); } diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index fec0006662d17..cb8812fda3e5b 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -80,7 +80,7 @@ public function getController(Request $request) $callable = $this->createController($controller); if (!is_callable($callable)) { - throw new \InvalidArgumentException(sprintf('Controller "%s" for URI "%s" is not callable.', $controller, $request->getPathInfo())); + throw new \InvalidArgumentException(sprintf('The controller for URI "%s" is not callable. %s', $request->getPathInfo(), $this->getControllerError($callable))); } return $callable; @@ -167,4 +167,65 @@ protected function instantiateController($class) { return new $class(); } + + private function getControllerError($callable) + { + if (is_string($callable)) { + if (false !== strpos($callable, '::')) { + $callable = explode('::', $callable); + } + + if (class_exists($callable) && !method_exists($callable, '__invoke')) { + return sprintf('Class "%s" does not have a method "__invoke".', $callable); + } + + if (!function_exists($callable)) { + return sprintf('Function "%s" does not exist.', $callable); + } + } + + if (!is_array($callable)) { + return sprintf('Invalid type for controller given, expected string or array, got "%s".', gettype($callable)); + } + + if (2 !== count($callable)) { + return sprintf('Invalid format for controller, expected array(controller, method) or controller::method.'); + } + + list($controller, $method) = $callable; + + if (is_string($controller) && !class_exists($controller)) { + return sprintf('Class "%s" does not exist.', $controller); + } + + $className = is_object($controller) ? get_class($controller) : $controller; + + if (method_exists($controller, $method)) { + return sprintf('Method "%s" on class "%s" should be public and non-abstract.', $method, $className); + } + + $collection = get_class_methods($controller); + + $alternatives = array(); + + foreach ($collection as $item) { + $lev = levenshtein($method, $item); + + if ($lev <= strlen($method) / 3 || false !== strpos($item, $method)) { + $alternatives[] = $item; + } + } + + asort($alternatives); + + $message = sprintf('Expected method "%s" on class "%s"', $method, $className); + + if (count($alternatives) > 0) { + $message .= sprintf(', did you mean "%s"?', implode('", "', $alternatives)); + } else { + $message .= sprintf('. Available methods: "%s".', implode('", "', $collection)); + } + + return $message; + } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php index 4b886ee10977d..d1fc9db8ecb43 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php @@ -110,12 +110,12 @@ public function testGetControllerWithFunction() } /** - * @dataProvider getUndefinedControllers - * @expectedException \InvalidArgumentException + * @dataProvider getUndefinedControllers */ - public function testGetControllerOnNonUndefinedFunction($controller) + public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null) { $resolver = $this->createControllerResolver(); + $this->setExpectedException($exceptionName, $exceptionMessage); $request = Request::create('/'); $request->attributes->set('_controller', $controller); @@ -125,10 +125,14 @@ public function testGetControllerOnNonUndefinedFunction($controller) public function getUndefinedControllers() { return array( - array('foo'), - array('foo::bar'), - array('stdClass'), - array('Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar'), + array(1, 'InvalidArgumentException', 'Unable to find controller "1".'), + array('foo', 'InvalidArgumentException', 'Unable to find controller "foo".'), + array('foo::bar', 'InvalidArgumentException', 'Class "foo" does not exist.'), + array('stdClass', 'InvalidArgumentException', 'Unable to find controller "stdClass".'), + array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::staticsAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Expected method "staticsAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest", did you mean "staticAction"?'), + array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::privateAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Method "privateAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'), + array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::protectedAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Method "protectedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest" should be public and non-abstract'), + array('Symfony\Component\HttpKernel\Tests\Controller\ControllerTest::undefinedAction', 'InvalidArgumentException', 'The controller for URI "/" is not callable. Expected method "undefinedAction" on class "Symfony\Component\HttpKernel\Tests\Controller\ControllerTest". Available methods: "publicAction", "staticAction"'), ); } @@ -236,3 +240,22 @@ protected function controllerMethod5(Request $request) function some_controller_function($foo, $foobar) { } + +class ControllerTest +{ + public function publicAction() + { + } + + private function privateAction() + { + } + + protected function protectedAction() + { + } + + public static function staticAction() + { + } +}