From 7bbae4159bcd5e49fb2303a7af6af99b45da498d Mon Sep 17 00:00:00 2001 From: Maxime Steinhausser Date: Sun, 26 Feb 2017 13:01:51 +0100 Subject: [PATCH] [HttpKernel] Add a ContainerControllerResolver (psr-11) --- .../Controller/ControllerResolver.php | 39 +---- .../Controller/ControllerResolverTest.php | 123 +-------------- .../ContainerControllerResolver.php | 76 +++++++++ .../ContainerControllerResolverTest.php | 148 ++++++++++++++++++ 4 files changed, 236 insertions(+), 150 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php index 31593b9b80..a6a7fe8a55 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php @@ -12,18 +12,17 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; use Psr\Log\LoggerInterface; -use Symfony\Component\HttpKernel\Controller\ControllerResolver as BaseControllerResolver; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver; /** * ControllerResolver. * * @author Fabien Potencier */ -class ControllerResolver extends BaseControllerResolver +class ControllerResolver extends ContainerControllerResolver { - protected $container; protected $parser; /** @@ -35,39 +34,19 @@ class ControllerResolver extends BaseControllerResolver */ public function __construct(ContainerInterface $container, ControllerNameParser $parser, LoggerInterface $logger = null) { - $this->container = $container; $this->parser = $parser; - parent::__construct($logger); + parent::__construct($container, $logger); } /** - * Returns a callable for the given controller. - * - * @param string $controller A Controller string - * - * @return mixed A PHP callable - * - * @throws \LogicException When the name could not be parsed - * @throws \InvalidArgumentException When the controller class does not exist + * {@inheritdoc} */ protected function createController($controller) { - if (false === strpos($controller, '::')) { - $count = substr_count($controller, ':'); - if (2 == $count) { - // controller in the a:b:c notation then - $controller = $this->parser->parse($controller); - } elseif (1 == $count) { - // controller in the service:method notation - list($service, $method) = explode(':', $controller, 2); - - return array($this->container->get($service), $method); - } elseif ($this->container->has($controller) && method_exists($service = $this->container->get($controller), '__invoke')) { - return $service; - } else { - throw new \LogicException(sprintf('Unable to parse the controller name "%s".', $controller)); - } + if (false === strpos($controller, '::') && 2 === substr_count($controller, ':')) { + // controller in the a:b:c notation then + $controller = $this->parser->parse($controller); } return parent::createController($controller); @@ -78,10 +57,6 @@ class ControllerResolver extends BaseControllerResolver */ protected function instantiateController($class) { - if ($this->container->has($class)) { - return $this->container->get($class); - } - $controller = parent::instantiateController($class); if ($controller instanceof ContainerAwareInterface) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index b3b592e5bb..0ccc4f5514 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -11,15 +11,16 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; +use Psr\Container\ContainerInterface as Psr11ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Bundle\FrameworkBundle\Controller\ControllerResolver; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; -use Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest as BaseControllerResolverTest; +use Symfony\Component\HttpKernel\Tests\Controller\ContainerControllerResolverTest; -class ControllerResolverTest extends BaseControllerResolverTest +class ControllerResolverTest extends ContainerControllerResolverTest { public function testGetControllerOnContainerAware() { @@ -55,7 +56,7 @@ class ControllerResolverTest extends BaseControllerResolverTest ->will($this->returnValue('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController::testAction')) ; - $resolver = $this->createControllerResolver(null, $parser); + $resolver = $this->createControllerResolver(null, null, $parser); $request = Request::create('/'); $request->attributes->set('_controller', $shortName); @@ -66,110 +67,7 @@ class ControllerResolverTest extends BaseControllerResolverTest $this->assertSame('testAction', $controller[1]); } - public function testGetControllerService() - { - $container = $this->createMockContainer(); - $container->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($this)) - ; - - $resolver = $this->createControllerResolver(null, null, $container); - $request = Request::create('/'); - $request->attributes->set('_controller', 'foo:controllerMethod1'); - - $controller = $resolver->getController($request); - - $this->assertInstanceOf(get_class($this), $controller[0]); - $this->assertSame('controllerMethod1', $controller[1]); - } - - public function testGetControllerInvokableService() - { - $invokableController = new InvokableController('bar'); - - $container = $this->createMockContainer(); - $container->expects($this->once()) - ->method('has') - ->with('foo') - ->will($this->returnValue(true)) - ; - $container->expects($this->once()) - ->method('get') - ->with('foo') - ->will($this->returnValue($invokableController)) - ; - - $resolver = $this->createControllerResolver(null, null, $container); - $request = Request::create('/'); - $request->attributes->set('_controller', 'foo'); - - $controller = $resolver->getController($request); - - $this->assertEquals($invokableController, $controller); - } - - public function testGetControllerInvokableServiceWithClassNameAsName() - { - $invokableController = new InvokableController('bar'); - $className = __NAMESPACE__.'\InvokableController'; - - $container = $this->createMockContainer(); - $container->expects($this->once()) - ->method('has') - ->with($className) - ->will($this->returnValue(true)) - ; - $container->expects($this->once()) - ->method('get') - ->with($className) - ->will($this->returnValue($invokableController)) - ; - - $resolver = $this->createControllerResolver(null, null, $container); - $request = Request::create('/'); - $request->attributes->set('_controller', $className); - - $controller = $resolver->getController($request); - - $this->assertEquals($invokableController, $controller); - } - - /** - * @dataProvider getUndefinedControllers - */ - public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null) - { - // All this logic needs to be duplicated, since calling parent::testGetControllerOnNonUndefinedFunction will override the expected excetion and not use the regex - $resolver = $this->createControllerResolver(); - if (method_exists($this, 'expectException')) { - $this->expectException($exceptionName); - $this->expectExceptionMessageRegExp($exceptionMessage); - } else { - $this->setExpectedExceptionRegExp($exceptionName, $exceptionMessage); - } - - $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('oof::bar', '\InvalidArgumentException', '/Class "oof" does not exist\./'), - array('stdClass', '\LogicException', '/Unable to parse the controller name "stdClass"\./'), - array( - 'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar', - '\InvalidArgumentException', - '/.?[cC]ontroller(.*?) for URI "\/" is not callable\.( Expected method(.*) Available methods)?/', - ), - ); - } - - protected function createControllerResolver(LoggerInterface $logger = null, ControllerNameParser $parser = null, ContainerInterface $container = null) + protected function createControllerResolver(LoggerInterface $logger = null, Psr11ContainerInterface $container = null, ControllerNameParser $parser = null) { if (!$parser) { $parser = $this->createMockParser(); @@ -215,14 +113,3 @@ class ContainerAwareController implements ContainerAwareInterface { } } - -class InvokableController -{ - public function __construct($bar) // mandatory argument to prevent automatic instantiation - { - } - - public function __invoke() - { - } -} diff --git a/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php new file mode 100644 index 0000000000..1a107c62f6 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ContainerControllerResolver.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; + +/** + * A controller resolver searching for a controller in a psr-11 container when using the "service:method" notation. + * + * @author Fabien Potencier + * @author Maxime Steinhausser + */ +class ContainerControllerResolver extends ControllerResolver +{ + protected $container; + + public function __construct(ContainerInterface $container, LoggerInterface $logger = null) + { + $this->container = $container; + + parent::__construct($logger); + } + + /** + * Returns a callable for the given controller. + * + * @param string $controller A Controller string + * + * @return mixed A PHP callable + * + * @throws \LogicException When the name could not be parsed + * @throws \InvalidArgumentException When the controller class does not exist + */ + protected function createController($controller) + { + if (false !== strpos($controller, '::')) { + return parent::createController($controller); + } + + if (1 == substr_count($controller, ':')) { + // controller in the "service:method" notation + list($service, $method) = explode(':', $controller, 2); + + return array($this->container->get($service), $method); + } + + if ($this->container->has($controller) && method_exists($service = $this->container->get($controller), '__invoke')) { + // invokable controller in the "service" notation + return $service; + } + + throw new \LogicException(sprintf('Unable to parse the controller name "%s".', $controller)); + } + + /** + * {@inheritdoc} + */ + protected function instantiateController($class) + { + if ($this->container->has($class)) { + return $this->container->get($class); + } + + return parent::instantiateController($class); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php new file mode 100644 index 0000000000..30b535e825 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ContainerControllerResolverTest.php @@ -0,0 +1,148 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\Controller; + +use Psr\Container\ContainerInterface; +use Psr\Log\LoggerInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ContainerControllerResolver; + +class ContainerControllerResolverTest extends ControllerResolverTest +{ + public function testGetControllerService() + { + $container = $this->createMockContainer(); + $container->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($this)) + ; + + $resolver = $this->createControllerResolver(null, $container); + $request = Request::create('/'); + $request->attributes->set('_controller', 'foo:controllerMethod1'); + + $controller = $resolver->getController($request); + + $this->assertInstanceOf(get_class($this), $controller[0]); + $this->assertSame('controllerMethod1', $controller[1]); + } + + public function testGetControllerInvokableService() + { + $invokableController = new InvokableController('bar'); + + $container = $this->createMockContainer(); + $container->expects($this->once()) + ->method('has') + ->with('foo') + ->will($this->returnValue(true)) + ; + $container->expects($this->once()) + ->method('get') + ->with('foo') + ->will($this->returnValue($invokableController)) + ; + + $resolver = $this->createControllerResolver(null, $container); + $request = Request::create('/'); + $request->attributes->set('_controller', 'foo'); + + $controller = $resolver->getController($request); + + $this->assertEquals($invokableController, $controller); + } + + public function testGetControllerInvokableServiceWithClassNameAsName() + { + $invokableController = new InvokableController('bar'); + $className = __NAMESPACE__.'\InvokableController'; + + $container = $this->createMockContainer(); + $container->expects($this->once()) + ->method('has') + ->with($className) + ->will($this->returnValue(true)) + ; + $container->expects($this->once()) + ->method('get') + ->with($className) + ->will($this->returnValue($invokableController)) + ; + + $resolver = $this->createControllerResolver(null, $container); + $request = Request::create('/'); + $request->attributes->set('_controller', $className); + + $controller = $resolver->getController($request); + + $this->assertEquals($invokableController, $controller); + } + + /** + * @dataProvider getUndefinedControllers + */ + public function testGetControllerOnNonUndefinedFunction($controller, $exceptionName = null, $exceptionMessage = null) + { + // All this logic needs to be duplicated, since calling parent::testGetControllerOnNonUndefinedFunction will override the expected excetion and not use the regex + $resolver = $this->createControllerResolver(); + if (method_exists($this, 'expectException')) { + $this->expectException($exceptionName); + $this->expectExceptionMessageRegExp($exceptionMessage); + } else { + $this->setExpectedExceptionRegExp($exceptionName, $exceptionMessage); + } + + $request = Request::create('/'); + $request->attributes->set('_controller', $controller); + $resolver->getController($request); + } + + public function getUndefinedControllers() + { + return array( + array('foo', \LogicException::class, '/Unable to parse the controller name "foo"\./'), + array('oof::bar', \InvalidArgumentException::class, '/Class "oof" does not exist\./'), + array('stdClass', \LogicException::class, '/Unable to parse the controller name "stdClass"\./'), + array( + 'Symfony\Component\HttpKernel\Tests\Controller\ControllerResolverTest::bar', + \InvalidArgumentException::class, + '/.?[cC]ontroller(.*?) for URI "\/" is not callable\.( Expected method(.*) Available methods)?/', + ), + ); + } + + protected function createControllerResolver(LoggerInterface $logger = null, ContainerInterface $container = null) + { + if (!$container) { + $container = $this->createMockContainer(); + } + + return new ContainerControllerResolver($container, $logger); + } + + protected function createMockContainer() + { + return $this->getMockBuilder(ContainerInterface::class)->getMock(); + } +} + +class InvokableController +{ + public function __construct($bar) // mandatory argument to prevent automatic instantiation + { + } + + public function __invoke() + { + } +}