From 360fc5fc4baa99b18d13730a59e4d96c2a21f2a3 Mon Sep 17 00:00:00 2001 From: Iltar van der Berg Date: Wed, 16 Mar 2016 08:56:41 +0100 Subject: [PATCH 1/5] Extracting arg resolving from ControllerResolver --- UPGRADE-3.1.md | 4 + .../FrameworkExtension.php | 4 +- .../Resources/config/debug.xml | 10 +- .../Resources/config/services.xml | 1 + .../FrameworkBundle/Resources/config/web.xml | 2 + .../Bundle/FrameworkBundle/composer.json | 2 +- src/Symfony/Component/HttpKernel/CHANGELOG.md | 3 + .../Controller/ArgumentResolver.php | 70 +++++++++ .../Controller/ArgumentResolverInterface.php | 35 +++++ .../Controller/ControllerResolver.php | 46 ++---- .../ControllerResolverInterface.php | 2 + .../Controller/TraceableArgumentResolver.php | 44 ++++++ .../TraceableControllerResolver.php | 25 +++- .../Component/HttpKernel/HttpKernel.php | 21 ++- .../Tests/Controller/ArgumentResolverTest.php | 137 ++++++++++++++++++ .../Controller/ControllerResolverTest.php | 4 + .../Debug/TraceableEventDispatcherTest.php | 10 +- .../Fragment/InlineFragmentRendererTest.php | 14 +- 18 files changed, 373 insertions(+), 61 deletions(-) create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php diff --git a/UPGRADE-3.1.md b/UPGRADE-3.1.md index 0d688dd08837f..3f7335781b2bf 100644 --- a/UPGRADE-3.1.md +++ b/UPGRADE-3.1.md @@ -65,6 +65,10 @@ HttpKernel * Passing objects as URI attributes to the ESI and SSI renderers has been deprecated and will be removed in Symfony 4.0. The inline fragment renderer should be used with object attributes. + * The `ControllerResolver::getArguments()` method is deprecated and will be + removed in 4.0. If you have your own `ControllerResolverInterface` + implementation, you should replace this method by implementing the + `ArgumentResolverInterface` and injecting it in the HttpKernel. Serializer ---------- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 64aad65afa18b..1421369ed51b9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -150,9 +150,6 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('debug.xml'); - $definition = $container->findDefinition('http_kernel'); - $definition->replaceArgument(1, new Reference('debug.controller_resolver')); - // replace the regular event_dispatcher service with the debug one $definition = $container->findDefinition('event_dispatcher'); $definition->setPublic(false); @@ -173,6 +170,7 @@ public function load(array $configs, ContainerBuilder $container) 'Symfony\\Component\\HttpKernel\\EventListener\\ResponseListener', 'Symfony\\Component\\HttpKernel\\EventListener\\RouterListener', 'Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver', + 'Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver', 'Symfony\\Component\\HttpKernel\\Event\\KernelEvent', 'Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent', 'Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent', diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml index d5d7855a23904..b6e7c0599bc7e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug.xml @@ -17,8 +17,14 @@ - - + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 6e8aeb90c5f6d..a5c0baba1b053 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -13,6 +13,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 7787e1df599f5..eab11f2dd76df 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -17,6 +17,8 @@ + + %kernel.charset% diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 86752accf1f6a..689aa7d6b65f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -23,7 +23,7 @@ "symfony/config": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/http-foundation": "~3.1", - "symfony/http-kernel": "~2.8|~3.0", + "symfony/http-kernel": "~3.1", "symfony/polyfill-mbstring": "~1.0", "symfony/filesystem": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index c41184b2c7d20..61414eaf1ea50 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -4,6 +4,9 @@ CHANGELOG 3.1.0 ----- * deprecated passing objects as URI attributes to the ESI and SSI renderers + * Added an `ArgumentResolver` with `getArguments()` and the respective interface `ArgumentResolverInterface` + * Deprecated `ControllerResolver::getArguments()`, which uses the `ArgumentResolver` as BC layer by extending it + * The `HttpKernel` now accepts an additional argument for an `ArgumentResolver` 3.0.0 ----- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php new file mode 100644 index 0000000000000..a7f4b3c731dde --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -0,0 +1,70 @@ + + * + * 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 Symfony\Component\HttpFoundation\Request; + +/** + * Responsible for the creation of the action arguments. + * + * @author Fabien Potencier + */ +class ArgumentResolver implements ArgumentResolverInterface +{ + /** + * {@inheritdoc} + */ + public function getArguments(Request $request, $controller) + { + if (is_array($controller)) { + $r = new \ReflectionMethod($controller[0], $controller[1]); + } elseif (is_object($controller) && !$controller instanceof \Closure) { + $r = new \ReflectionObject($controller); + $r = $r->getMethod('__invoke'); + } else { + $r = new \ReflectionFunction($controller); + } + + return $this->doGetArguments($request, $controller, $r->getParameters()); + } + + protected function doGetArguments(Request $request, $controller, array $parameters) + { + $attributes = $request->attributes->all(); + $arguments = array(); + foreach ($parameters as $param) { + if (array_key_exists($param->name, $attributes)) { + if (PHP_VERSION_ID >= 50600 && $param->isVariadic() && is_array($attributes[$param->name])) { + $arguments = array_merge($arguments, array_values($attributes[$param->name])); + } else { + $arguments[] = $attributes[$param->name]; + } + } elseif ($param->getClass() && $param->getClass()->isInstance($request)) { + $arguments[] = $request; + } elseif ($param->isDefaultValueAvailable()) { + $arguments[] = $param->getDefaultValue(); + } else { + if (is_array($controller)) { + $repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]); + } elseif (is_object($controller)) { + $repr = get_class($controller); + } else { + $repr = $controller; + } + + throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name)); + } + } + + return $arguments; + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php new file mode 100644 index 0000000000000..19d23aef841ae --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php @@ -0,0 +1,35 @@ + + * + * 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 Symfony\Component\HttpFoundation\Request; + +/** + * An ArgumentResolverInterface implementation knows how to determine the + * arguments for a specific action. + * + * @author Fabien Potencier + */ +interface ArgumentResolverInterface +{ + /** + * Returns the arguments to pass to the controller. + * + * @param Request $request A Request instance + * @param callable $controller A PHP callable + * + * @return array An array of arguments to pass to the controller + * + * @throws \RuntimeException When value for argument given is not provided + */ + public function getArguments(Request $request, $controller); +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index 56d1b97afdd4b..ead07f702bee4 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -23,7 +23,7 @@ * * @author Fabien Potencier */ -class ControllerResolver implements ControllerResolverInterface +class ControllerResolver extends ArgumentResolver implements ControllerResolverInterface { private $logger; @@ -84,50 +84,24 @@ public function getController(Request $request) /** * {@inheritdoc} + * + * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the ArgumentResolver instead. */ public function getArguments(Request $request, $controller) { - if (is_array($controller)) { - $r = new \ReflectionMethod($controller[0], $controller[1]); - } elseif (is_object($controller) && !$controller instanceof \Closure) { - $r = new \ReflectionObject($controller); - $r = $r->getMethod('__invoke'); - } else { - $r = new \ReflectionFunction($controller); - } + @trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED); - return $this->doGetArguments($request, $controller, $r->getParameters()); + return parent::getArguments($request, $controller); } + /** + * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the ArgumentResolver instead. + */ protected function doGetArguments(Request $request, $controller, array $parameters) { - $attributes = $request->attributes->all(); - $arguments = array(); - foreach ($parameters as $param) { - if (array_key_exists($param->name, $attributes)) { - if (PHP_VERSION_ID >= 50600 && $param->isVariadic() && is_array($attributes[$param->name])) { - $arguments = array_merge($arguments, array_values($attributes[$param->name])); - } else { - $arguments[] = $attributes[$param->name]; - } - } elseif ($param->getClass() && $param->getClass()->isInstance($request)) { - $arguments[] = $request; - } elseif ($param->isDefaultValueAvailable()) { - $arguments[] = $param->getDefaultValue(); - } else { - if (is_array($controller)) { - $repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]); - } elseif (is_object($controller)) { - $repr = get_class($controller); - } else { - $repr = $controller; - } - - throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name)); - } - } + @trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED); - return $arguments; + return parent::doGetArguments($request, $controller, $parameters); } /** diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php index f7b19ed1bdbac..0dd7cce96d905 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolverInterface.php @@ -52,6 +52,8 @@ public function getController(Request $request); * @return array An array of arguments to pass to the controller * * @throws \RuntimeException When value for argument given is not provided + * + * @deprecated This method is deprecated as of 3.1 and will be removed in 4.0. Please use the {@see ArgumentResolverInterface} instead. */ public function getArguments(Request $request, $controller); } diff --git a/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php new file mode 100644 index 0000000000000..6fb0fa66aca7a --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/TraceableArgumentResolver.php @@ -0,0 +1,44 @@ + + * + * 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 Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\HttpFoundation\Request; + +/** + * @author Fabien Potencier + */ +class TraceableArgumentResolver implements ArgumentResolverInterface +{ + private $resolver; + private $stopwatch; + + public function __construct(ArgumentResolverInterface $resolver, Stopwatch $stopwatch) + { + $this->resolver = $resolver; + $this->stopwatch = $stopwatch; + } + + /** + * {@inheritdoc} + */ + public function getArguments(Request $request, $controller) + { + $e = $this->stopwatch->start('controller.get_arguments'); + + $ret = $this->resolver->getArguments($request, $controller); + + $e->stop(); + + return $ret; + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php index f8de31cf078c1..469668ce7f5e5 100644 --- a/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php @@ -19,21 +19,28 @@ * * @author Fabien Potencier */ -class TraceableControllerResolver implements ControllerResolverInterface +class TraceableControllerResolver implements ControllerResolverInterface, ArgumentResolverInterface { private $resolver; private $stopwatch; + private $argumentResolver; /** * Constructor. * - * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance - * @param Stopwatch $stopwatch A Stopwatch instance + * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance + * @param Stopwatch $stopwatch A Stopwatch instance + * @param ArgumentResolverInterface $argumentResolver Only required for BC */ - public function __construct(ControllerResolverInterface $resolver, Stopwatch $stopwatch) + public function __construct(ControllerResolverInterface $resolver, Stopwatch $stopwatch, ArgumentResolverInterface $argumentResolver = null) { $this->resolver = $resolver; $this->stopwatch = $stopwatch; + $this->argumentResolver = $argumentResolver; + + if (null === $this->argumentResolver) { + $this->argumentResolver = $resolver; + } } /** @@ -52,12 +59,20 @@ public function getController(Request $request) /** * {@inheritdoc} + * + * @deprecated This method is deprecated as of 3.1 and will be removed in 4.0. */ public function getArguments(Request $request, $controller) { + @trigger_error(sprintf('This %s method is deprecated as of 3.1 and will be removed in 4.0. Please use the %s instead.', __METHOD__, TraceableArgumentResolver::class), E_USER_DEPRECATED); + + if ($this->argumentResolver instanceof TraceableArgumentResolver) { + return $this->argumentResolver->getArguments($request, $controller); + } + $e = $this->stopwatch->start('controller.get_arguments'); - $ret = $this->resolver->getArguments($request, $controller); + $ret = $this->argumentResolver->getArguments($request, $controller); $e->stop(); diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index 1600b2ce591dd..c9b1d65227f4b 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; @@ -36,19 +38,28 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface protected $dispatcher; protected $resolver; protected $requestStack; + private $argumentResolver; /** * Constructor. * - * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance - * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance - * @param RequestStack $requestStack A stack for master/sub requests + * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance + * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance + * @param RequestStack $requestStack A stack for master/sub requests + * @param ArgumentResolverInterface $argumentResolver An ArgumentResolverInterface instance */ - public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, RequestStack $requestStack = null) + public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, RequestStack $requestStack = null, ArgumentResolverInterface $argumentResolver = null) { $this->dispatcher = $dispatcher; $this->resolver = $resolver; $this->requestStack = $requestStack ?: new RequestStack(); + $this->argumentResolver = $argumentResolver; + + if (null === $this->argumentResolver) { + @trigger_error(sprintf('As of 3.1 an %s is used to resolve arguments. In 4.0 the $argumentResolver becomes mandatory and the %s can no longer be used to resolve arguments.', ArgumentResolverInterface::class, ControllerResolverInterface::class), E_USER_DEPRECATED); + // fallback in case of deprecations + $this->argumentResolver = $resolver; + } } /** @@ -133,7 +144,7 @@ private function handleRaw(Request $request, $type = self::MASTER_REQUEST) $controller = $event->getController(); // controller arguments - $arguments = $this->resolver->getArguments($request, $controller); + $arguments = $this->argumentResolver->getArguments($request, $controller); // call controller $response = call_user_func_array($controller, $arguments); diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php new file mode 100644 index 0000000000000..4945b8a1a10ab --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php @@ -0,0 +1,137 @@ + + * + * 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 Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ControllerResolver; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; +use Symfony\Component\HttpFoundation\Request; + +class ArgumentResolverTest extends \PHPUnit_Framework_TestCase +{ + /** + * @group legacy + */ + public function testGetArguments() + { + $resolver = new ArgumentResolver(); + + $request = Request::create('/'); + $controller = array(new self(), 'testGetArguments'); + $this->assertEquals(array(), $resolver->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments'); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = array(new self(), 'controllerMethod1'); + $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method'); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = array(new self(), 'controllerMethod2'); + $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller), '->getArguments() uses default values if present'); + + $request->attributes->set('bar', 'bar'); + $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes'); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = function ($foo) {}; + $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = function ($foo, $bar = 'bar') {}; + $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = new self(); + $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller)); + $request->attributes->set('bar', 'bar'); + $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('foobar', 'foobar'); + $controller = 'Symfony\Component\HttpKernel\Tests\Controller\another_controller_function'; + $this->assertEquals(array('foo', 'foobar'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('foobar', 'foobar'); + $controller = array(new self(), 'controllerMethod3'); + + try { + $resolver->getArguments($request, $controller); + $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + } catch (\Exception $e) { + $this->assertInstanceOf('\RuntimeException', $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + } + + $request = Request::create('/'); + $controller = array(new self(), 'controllerMethod5'); + $this->assertEquals(array($request), $resolver->getArguments($request, $controller), '->getArguments() injects the request'); + } + + /** + * @requires PHP 5.6 + * @group legacy + */ + public function testGetVariadicArguments() + { + $resolver = new ControllerResolver(); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', array('foo', 'bar')); + $controller = array(new VariadicController(), 'action'); + $this->assertEquals(array('foo', 'foo', 'bar'), $resolver->getArguments($request, $controller)); + } + + public function testCreateControllerCanReturnAnyCallable() + { + $mock = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolver', array('createController')); + $mock->expects($this->once())->method('createController')->will($this->returnValue('Symfony\Component\HttpKernel\Tests\Controller\another_controller_function')); + + $request = Request::create('/'); + $request->attributes->set('_controller', 'foobar'); + $mock->getController($request); + } + + public function __invoke($foo, $bar = null) + { + } + + public function controllerMethod1($foo) + { + } + + protected function controllerMethod2($foo, $bar = null) + { + } + + protected function controllerMethod3($foo, $bar, $foobar) + { + } + + protected static function controllerMethod4() + { + } + + protected function controllerMethod5(Request $request) + { + } +} + +function another_controller_function($foo, $foobar) +{ +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php index 7ba1ff6e451d4..1e5fde618aa96 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ControllerResolverTest.php @@ -137,6 +137,9 @@ public function getUndefinedControllers() ); } + /** + * @group legacy + */ public function testGetArguments() { $resolver = $this->createControllerResolver(); @@ -200,6 +203,7 @@ public function testGetArguments() /** * @requires PHP 5.6 + * @group legacy */ public function testGetVariadicArguments() { diff --git a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php index f64d7247c0b48..5894ffbb8d744 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Debug/TraceableEventDispatcherTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\HttpKernel\Tests\Debug; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpFoundation\Request; @@ -108,10 +109,11 @@ public function testListenerCanRemoveItselfWhenExecuted() protected function getHttpKernel($dispatcher, $controller) { - $resolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); - $resolver->expects($this->once())->method('getController')->will($this->returnValue($controller)); - $resolver->expects($this->once())->method('getArguments')->will($this->returnValue(array())); + $controllerResolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolverInterface'); + $controllerResolver->expects($this->once())->method('getController')->will($this->returnValue($controller)); + $argumentResolver = $this->getMock('Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface'); + $argumentResolver->expects($this->once())->method('getArguments')->will($this->returnValue(array())); - return new HttpKernel($dispatcher, $resolver); + return new HttpKernel($dispatcher, $controllerResolver, new RequestStack(), $argumentResolver); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php index 4e487a478a600..ee25b83167496 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/InlineFragmentRendererTest.php @@ -11,6 +11,8 @@ namespace Symfony\Component\HttpKernel\Tests\Fragment; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer; @@ -60,7 +62,7 @@ public function testRenderWithObjectsAsAttributesPassedAsObjectsInTheController( })) ; - $kernel = new HttpKernel(new EventDispatcher(), $resolver); + $kernel = new HttpKernel(new EventDispatcher(), $resolver, new RequestStack(), new ArgumentResolver()); $renderer = new InlineFragmentRenderer($kernel); $response = $renderer->render(new ControllerReference('main_controller', array('object' => new \stdClass(), 'object1' => new Bar()), array()), Request::create('/')); @@ -142,8 +144,8 @@ private function getKernelExpectingRequest(Request $request) public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() { - $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $resolver + $controllerResolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); + $controllerResolver ->expects($this->once()) ->method('getController') ->will($this->returnValue(function () { @@ -152,13 +154,15 @@ public function testExceptionInSubRequestsDoesNotMangleOutputBuffers() throw new \RuntimeException(); })) ; - $resolver + + $argumentResolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolverInterface'); + $argumentResolver ->expects($this->once()) ->method('getArguments') ->will($this->returnValue(array())) ; - $kernel = new HttpKernel(new EventDispatcher(), $resolver); + $kernel = new HttpKernel(new EventDispatcher(), $controllerResolver, new RequestStack(), $argumentResolver); $renderer = new InlineFragmentRenderer($kernel); // simulate a main request with output buffering From cfcf764d24582e23c5a4ca1be7af98fd6220dc4c Mon Sep 17 00:00:00 2001 From: Iltar van der Berg Date: Fri, 25 Mar 2016 10:46:04 +0100 Subject: [PATCH 2/5] Added an ArgumentResolver with clean extension point --- UPGRADE-3.1.md | 4 +- .../ControllerArgumentValueResolverPass.php | 65 +++++++++ .../FrameworkExtension.php | 2 + .../FrameworkBundle/FrameworkBundle.php | 2 + .../FrameworkBundle/Resources/config/web.xml | 23 ++- src/Symfony/Component/HttpKernel/CHANGELOG.md | 7 +- .../Controller/ArgumentResolver.php | 77 +++++----- .../Controller/ArgumentResolverInterface.php | 4 +- .../ArgumentFromAttributeResolver.php | 40 ++++++ .../DefaultArgumentValueResolver.php | 40 ++++++ .../ArgumentValueResolver/RequestResolver.php | 40 ++++++ .../VariadicArgumentValueResolver.php | 48 +++++++ .../ArgumentValueResolverInterface.php | 45 ++++++ .../Controller/ControllerResolver.php | 10 +- .../Controller/LegacyArgumentResolver.php | 75 ++++++++++ .../TraceableControllerResolver.php | 1 + .../ControllerMetadata/ArgumentMetadata.php | 98 +++++++++++++ .../ArgumentMetadataFactory.php | 109 ++++++++++++++ .../ArgumentMetadataFactoryInterface.php | 27 ++++ .../Component/HttpKernel/HttpKernel.php | 9 -- .../Tests/Controller/ArgumentResolverTest.php | 78 ++++++++-- .../Controller/LegacyArgumentResolverTest.php | 123 ++++++++++++++++ .../ArgumentMetadataFactoryTest.php | 133 ++++++++++++++++++ .../RequestDataCollectorTest.php | 3 +- .../Controller/BasicTypesController.php | 10 ++ .../Fragment/InlineFragmentRendererTest.php | 40 +++++- .../Tests/HttpCache/TestHttpKernel.php | 5 +- .../HttpCache/TestMultipleHttpKernel.php | 5 +- .../HttpKernel/Tests/HttpKernelTest.php | 59 ++++---- .../HttpKernel/Tests/TestHttpKernel.php | 5 +- 30 files changed, 1083 insertions(+), 104 deletions(-) create mode 100644 src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ControllerArgumentValueResolverPass.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php create mode 100644 src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php create mode 100644 src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php create mode 100644 src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php create mode 100644 src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Controller/LegacyArgumentResolverTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/BasicTypesController.php diff --git a/UPGRADE-3.1.md b/UPGRADE-3.1.md index 3f7335781b2bf..b3de5e123a88e 100644 --- a/UPGRADE-3.1.md +++ b/UPGRADE-3.1.md @@ -65,10 +65,12 @@ HttpKernel * Passing objects as URI attributes to the ESI and SSI renderers has been deprecated and will be removed in Symfony 4.0. The inline fragment renderer should be used with object attributes. + * The `ControllerResolver::getArguments()` method is deprecated and will be removed in 4.0. If you have your own `ControllerResolverInterface` implementation, you should replace this method by implementing the - `ArgumentResolverInterface` and injecting it in the HttpKernel. + `ArgumentResolverInterface` and injecting it in the `HttpKernel`, or using + the `ArgumentResolver` and injecting this in the `HttpKernel`. Serializer ---------- diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ControllerArgumentValueResolverPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ControllerArgumentValueResolverPass.php new file mode 100644 index 0000000000000..a378054378870 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ControllerArgumentValueResolverPass.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * Gathers and configures the argument value resolvers. + * + * @author Iltar van der Berg + */ +class ControllerArgumentValueResolverPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('argument_resolver')) { + return; + } + + $definition = $container->getDefinition('argument_resolver'); + $argumentResolvers = $this->findAndSortTaggedServices('controller_argument.value_resolver', $container); + $definition->replaceArgument(1, $argumentResolvers); + } + + /** + * Finds all services with the given tag name and order them by their priority. + * + * @param string $tagName + * @param ContainerBuilder $container + * + * @return array + */ + private function findAndSortTaggedServices($tagName, ContainerBuilder $container) + { + $services = $container->findTaggedServiceIds($tagName); + + $sortedServices = array(); + foreach ($services as $serviceId => $tags) { + foreach ($tags as $attributes) { + $priority = isset($attributes['priority']) ? $attributes['priority'] : 0; + $sortedServices[$priority][] = new Reference($serviceId); + } + } + + if (empty($sortedServices)) { + return array(); + } + + krsort($sortedServices); + + // Flatten the array + return call_user_func_array('array_merge', $sortedServices); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 1421369ed51b9..22b8dde6b3d56 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -171,6 +171,8 @@ public function load(array $configs, ContainerBuilder $container) 'Symfony\\Component\\HttpKernel\\EventListener\\RouterListener', 'Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver', 'Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver', + 'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata', + 'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadataFactory', 'Symfony\\Component\\HttpKernel\\Event\\KernelEvent', 'Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent', 'Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent', diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index 94062da0039f5..98002fb6b6a5d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ControllerArgumentValueResolverPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; @@ -87,6 +88,7 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new FragmentRendererPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new SerializerPass()); $container->addCompilerPass(new PropertyInfoPass()); + $container->addCompilerPass(new ControllerArgumentValueResolverPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index eab11f2dd76df..4abcdc4377028 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -17,7 +17,28 @@ - + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index 61414eaf1ea50..d430cb1b13e19 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -4,9 +4,10 @@ CHANGELOG 3.1.0 ----- * deprecated passing objects as URI attributes to the ESI and SSI renderers - * Added an `ArgumentResolver` with `getArguments()` and the respective interface `ArgumentResolverInterface` - * Deprecated `ControllerResolver::getArguments()`, which uses the `ArgumentResolver` as BC layer by extending it - * The `HttpKernel` now accepts an additional argument for an `ArgumentResolver` + * Added a `LegacyArgumentResolver` with `getArguments()` and the corresponding interface `ArgumentResolverInterface` + * Deprecated `ControllerResolver::getArguments()`, which uses the `LegacyArgumentResolver` as BC layer by extending it + * The `HttpKernel` now accepts an additional argument for an `ArgumentResolverInterface` + * Added the `ArgumentResolver` which features an extension point to resolve arguments in a more dynamic way 3.0.0 ----- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index a7f4b3c731dde..673ab90c68016 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -12,57 +12,64 @@ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; /** - * Responsible for the creation of the action arguments. + * Responsible for the resolving of arguments passed to an action. * - * @author Fabien Potencier + * @author Iltar van der Berg */ -class ArgumentResolver implements ArgumentResolverInterface +final class ArgumentResolver implements ArgumentResolverInterface { + private $argumentMetadataFactory; + /** - * {@inheritdoc} + * @var ArgumentValueResolverInterface[] */ - public function getArguments(Request $request, $controller) - { - if (is_array($controller)) { - $r = new \ReflectionMethod($controller[0], $controller[1]); - } elseif (is_object($controller) && !$controller instanceof \Closure) { - $r = new \ReflectionObject($controller); - $r = $r->getMethod('__invoke'); - } else { - $r = new \ReflectionFunction($controller); - } + private $argumentValueResolvers; - return $this->doGetArguments($request, $controller, $r->getParameters()); + public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array()) + { + $this->argumentMetadataFactory = $argumentMetadataFactory; + $this->argumentValueResolvers = $argumentValueResolvers; } - protected function doGetArguments(Request $request, $controller, array $parameters) + /** + * {@inheritdoc} + */ + public function getArguments(Request $request, $controller) { - $attributes = $request->attributes->all(); $arguments = array(); - foreach ($parameters as $param) { - if (array_key_exists($param->name, $attributes)) { - if (PHP_VERSION_ID >= 50600 && $param->isVariadic() && is_array($attributes[$param->name])) { - $arguments = array_merge($arguments, array_values($attributes[$param->name])); - } else { - $arguments[] = $attributes[$param->name]; + + foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) { + foreach ($this->argumentValueResolvers as $resolver) { + if (!$resolver->supports($request, $metadata)) { + continue; } - } elseif ($param->getClass() && $param->getClass()->isInstance($request)) { - $arguments[] = $request; - } elseif ($param->isDefaultValueAvailable()) { - $arguments[] = $param->getDefaultValue(); - } else { - if (is_array($controller)) { - $repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]); - } elseif (is_object($controller)) { - $repr = get_class($controller); - } else { - $repr = $controller; + + $resolved = $resolver->resolve($request, $metadata); + + if (!$resolved instanceof \Generator) { + throw new \InvalidArgumentException(sprintf('%s::resolve() must yield at least one value.', get_class($resolver))); + } + + foreach ($resolved as $append) { + $arguments[] = $append; } - throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name)); + // continue to the next controller argument + continue 2; } + + $representative = $controller; + + if (is_array($representative)) { + $representative = sprintf('%s::%s()', get_class($representative[0]), $representative[1]); + } elseif (is_object($representative)) { + $representative = get_class($representative); + } + + throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $representative, $metadata->getName())); } return $arguments; diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php index 19d23aef841ae..dce129ee7ffa3 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php @@ -24,8 +24,8 @@ interface ArgumentResolverInterface /** * Returns the arguments to pass to the controller. * - * @param Request $request A Request instance - * @param callable $controller A PHP callable + * @param Request $request + * @param callable $controller * * @return array An array of arguments to pass to the controller * diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php new file mode 100644 index 0000000000000..0234db0e7b24c --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Grabs a non-variadic value from the request and returns it. + * + * @author Iltar van der Berg + */ +final class ArgumentFromAttributeResolver implements ArgumentValueResolverInterface +{ + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument) + { + return !$argument->isVariadic() && $request->attributes->has($argument->getName()); + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument) + { + yield $request->attributes->get($argument->getName()); + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php new file mode 100644 index 0000000000000..36db486e05962 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Returns the default value defined in the action signature if present and no value has been given. + * + * @author Iltar van der Berg + */ +final class DefaultArgumentValueResolver implements ArgumentValueResolverInterface +{ + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument) + { + return $argument->hasDefaultValue() && !$request->attributes->has($argument->getName()); + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument) + { + yield $argument->getDefaultValue(); + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php new file mode 100644 index 0000000000000..f7eff444b41dd --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php @@ -0,0 +1,40 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Supports the same instance as the request object passed along. + * + * @author Iltar van der Berg + */ +final class RequestResolver implements ArgumentValueResolverInterface +{ + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument) + { + return $argument->getType() === Request::class || is_subclass_of($request, $argument->getType()); + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument) + { + yield $request; + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php new file mode 100644 index 0000000000000..3cb302c515519 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Grabs the variadic value from the request and returns it. + * + * @author Iltar van der Berg + */ +final class VariadicArgumentValueResolver implements ArgumentValueResolverInterface +{ + /** + * {@inheritdoc} + */ + public function supports(Request $request, ArgumentMetadata $argument) + { + return $argument->isVariadic() && $request->attributes->has($argument->getName()); + } + + /** + * {@inheritdoc} + */ + public function resolve(Request $request, ArgumentMetadata $argument) + { + $values = $request->attributes->get($argument->getName()); + + if (!is_array($values)) { + throw new \InvalidArgumentException(sprintf('The action argument "...$%1$s" is required to be an array, the request attribute "%1$s" contains a type of "%2$s" instead.', $argument->getName(), gettype($values))); + } + + foreach ($values as $value) { + yield $value; + } + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php new file mode 100644 index 0000000000000..278fe4754dc5f --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php @@ -0,0 +1,45 @@ + + * + * 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 Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +/** + * Responsible for resolving the value of an argument based on its metadata. + * + * @author Iltar van der Berg + */ +interface ArgumentValueResolverInterface +{ + /** + * Whether this resolver can resolve can resolve the value for the given ArgumentMetadata. + * + * @param Request $request + * @param ArgumentMetadata $argument + * + * @return bool + */ + public function supports(Request $request, ArgumentMetadata $argument); + + /** + * Yield the possible value(s). + * + * An implementation must yield at least one value. + * + * @param Request $request + * @param ArgumentMetadata $argument + * + * @return \Generator + */ + public function resolve(Request $request, ArgumentMetadata $argument); +} diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index ead07f702bee4..13467d30e516b 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -23,7 +23,7 @@ * * @author Fabien Potencier */ -class ControllerResolver extends ArgumentResolver implements ControllerResolverInterface +class ControllerResolver extends LegacyArgumentResolver implements ControllerResolverInterface { private $logger; @@ -85,21 +85,21 @@ public function getController(Request $request) /** * {@inheritdoc} * - * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the ArgumentResolver instead. + * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead. */ public function getArguments(Request $request, $controller) { - @trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED); + @trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, LegacyArgumentResolver::class), E_USER_DEPRECATED); return parent::getArguments($request, $controller); } /** - * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the ArgumentResolver instead. + * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead. */ protected function doGetArguments(Request $request, $controller, array $parameters) { - @trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED); + @trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, LegacyArgumentResolver::class), E_USER_DEPRECATED); return parent::doGetArguments($request, $controller, $parameters); } diff --git a/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php new file mode 100644 index 0000000000000..22ee655152894 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php @@ -0,0 +1,75 @@ + + * + * 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 Symfony\Component\HttpFoundation\Request; + +/** + * Responsible for the creation of the action arguments. + * + * @deprecated This class is deprecated since 3.1 and will be removed in 4.0. Please use the ArgumentResolver::class instead. + * + * @author Fabien Potencier + */ +class LegacyArgumentResolver implements ArgumentResolverInterface +{ + /** + * {@inheritdoc} + */ + public function getArguments(Request $request, $controller) + { + // only trigger the deprecation notice if actually used, the ControllerResolver still extends this for BC reasons + @trigger_error(sprintf('The %s class is deprecated since 3.1 and will be removed in 4.0. Please use the %s instead.', __CLASS__, ArgumentResolver::class), E_USER_DEPRECATED); + + if (is_array($controller)) { + $r = new \ReflectionMethod($controller[0], $controller[1]); + } elseif (is_object($controller) && !$controller instanceof \Closure) { + $r = new \ReflectionObject($controller); + $r = $r->getMethod('__invoke'); + } else { + $r = new \ReflectionFunction($controller); + } + + return $this->doGetArguments($request, $controller, $r->getParameters()); + } + + protected function doGetArguments(Request $request, $controller, array $parameters) + { + $attributes = $request->attributes->all(); + $arguments = array(); + foreach ($parameters as $param) { + if (array_key_exists($param->name, $attributes)) { + if (PHP_VERSION_ID >= 50600 && $param->isVariadic() && is_array($attributes[$param->name])) { + $arguments = array_merge($arguments, array_values($attributes[$param->name])); + } else { + $arguments[] = $attributes[$param->name]; + } + } elseif ($param->getClass() && $param->getClass()->isInstance($request)) { + $arguments[] = $request; + } elseif ($param->isDefaultValueAvailable()) { + $arguments[] = $param->getDefaultValue(); + } else { + if (is_array($controller)) { + $repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]); + } elseif (is_object($controller)) { + $repr = get_class($controller); + } else { + $repr = $controller; + } + + throw new \RuntimeException(sprintf('Controller "%s" requires that you provide a value for the "$%s" argument (because there is no default value or because there is a non optional argument after this one).', $repr, $param->name)); + } + } + + return $arguments; + } +} diff --git a/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php index 469668ce7f5e5..78571fb0c82fd 100644 --- a/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php @@ -38,6 +38,7 @@ public function __construct(ControllerResolverInterface $resolver, Stopwatch $st $this->stopwatch = $stopwatch; $this->argumentResolver = $argumentResolver; + // required for BC reasons if (null === $this->argumentResolver) { $this->argumentResolver = $resolver; } diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php new file mode 100644 index 0000000000000..71f05fab4b4ea --- /dev/null +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php @@ -0,0 +1,98 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\ControllerMetadata; + +/** + * Responsible for storing metadata of an argument. + * + * @author Iltar van der Berg + */ +class ArgumentMetadata +{ + private $name; + private $type; + private $isVariadic; + private $hasDefaultValue; + private $defaultValue; + + /** + * @param string $name + * @param string $type + * @param bool $isVariadic + * @param bool $hasDefaultValue + * @param mixed $defaultValue + */ + public function __construct($name, $type, $isVariadic, $hasDefaultValue, $defaultValue) + { + $this->name = $name; + $this->type = $type; + $this->isVariadic = $isVariadic; + $this->hasDefaultValue = $hasDefaultValue; + $this->defaultValue = $defaultValue; + } + + /** + * Returns the name as given in PHP, $foo would yield "foo". + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the type of the argument. + * + * The type is the PHP class in 5.5+ and additionally the basic type in PHP 7.0+. + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Returns whether the argument is defined as "...$variadic". + * + * @return bool + */ + public function isVariadic() + { + return $this->isVariadic; + } + + /** + * Returns whether the argument has a default value. + * + * Implies whether an argument is optional. + * + * @return bool + */ + public function hasDefaultValue() + { + return $this->hasDefaultValue; + } + + /** + * Returns the default value of the argument. + * + * Make sure to call {@see self::hasDefaultValue()} first to see if a default value is possible. + * + * @return mixed + */ + public function getDefaultValue() + { + return $this->defaultValue; + } +} diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php new file mode 100644 index 0000000000000..0c669806127da --- /dev/null +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\ControllerMetadata; + +/** + * Builds {@see ArgumentMetadata} objects based on the given Controller. + * + * @author Iltar van der Berg + */ +final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface +{ + /** + * {@inheritdoc} + */ + public function createArgumentMetadata($controller) + { + $arguments = array(); + + if (is_array($controller)) { + $reflection = new \ReflectionMethod($controller[0], $controller[1]); + } elseif (is_object($controller) && !$controller instanceof \Closure) { + $reflection = (new \ReflectionObject($controller))->getMethod('__invoke'); + } else { + $reflection = new \ReflectionFunction($controller); + } + + foreach ($reflection->getParameters() as $param) { + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $this->isVariadic($param), $this->hasDefaulValue($param), $this->getDefaulValue($param)); + } + + return $arguments; + } + + /** + * Returns whether an argument is variadic. + * + * @param \ReflectionParameter $parameter + * + * @return bool + */ + private function isVariadic(\ReflectionParameter $parameter) + { + return PHP_VERSION_ID >= 50600 && $parameter->isVariadic(); + } + + /** + * Determines whether an argument has a default value. + * + * @param \ReflectionParameter $parameter + * + * @return bool + */ + private function hasDefaulValue(\ReflectionParameter $parameter) + { + return $parameter->isDefaultValueAvailable(); + } + + /** + * Returns a default value if available. + * + * @param \ReflectionParameter $parameter + * + * @return mixed|null + */ + private function getDefaulValue(\ReflectionParameter $parameter) + { + return $this->hasDefaulValue($parameter) ? $parameter->getDefaultValue() : null; + } + + /** + * Returns an associated type to the given parameter if available. + * + * @param \ReflectionParameter $parameter + * + * @return null|string + */ + private function getType(\ReflectionParameter $parameter) + { + if (PHP_VERSION_ID >= 70000) { + return $parameter->hasType() ? (string) $parameter->getType() : null; + } + + if ($parameter->isArray()) { + return 'array'; + } + + if ($parameter->isCallable()) { + return 'callable'; + } + + try { + $refClass = $parameter->getClass(); + } catch (\ReflectionException $e) { + // mandatory; extract it from the exception message + return str_replace(['Class ', ' does not exist'], '', $e->getMessage()); + } + + return $refClass ? $refClass->getName() : null; + } +} diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php new file mode 100644 index 0000000000000..5045d9dc0141b --- /dev/null +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactoryInterface.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\ControllerMetadata; + +/** + * Builds method argument data. + * + * @author Iltar van der Berg + */ +interface ArgumentMetadataFactoryInterface +{ + /** + * @param mixed $controller The controller to resolve the arguments for. + * + * @return ArgumentMetadata[] + */ + public function createArgumentMetadata($controller); +} diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index c9b1d65227f4b..d077623068888 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -11,7 +11,6 @@ namespace Symfony\Component\HttpKernel; -use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -40,14 +39,6 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface protected $requestStack; private $argumentResolver; - /** - * Constructor. - * - * @param EventDispatcherInterface $dispatcher An EventDispatcherInterface instance - * @param ControllerResolverInterface $resolver A ControllerResolverInterface instance - * @param RequestStack $requestStack A stack for master/sub requests - * @param ArgumentResolverInterface $argumentResolver An ArgumentResolverInterface instance - */ public function __construct(EventDispatcherInterface $dispatcher, ControllerResolverInterface $resolver, RequestStack $requestStack = null, ArgumentResolverInterface $argumentResolver = null) { $this->dispatcher = $dispatcher; diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php index 4945b8a1a10ab..18916f56972c3 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php @@ -12,18 +12,28 @@ namespace Symfony\Component\HttpKernel\Tests\Controller; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; -use Symfony\Component\HttpKernel\Controller\ControllerResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\ArgumentFromAttributeResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\DefaultArgumentValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\RequestResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\VariadicArgumentValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; use Symfony\Component\HttpFoundation\Request; class ArgumentResolverTest extends \PHPUnit_Framework_TestCase { - /** - * @group legacy - */ public function testGetArguments() { - $resolver = new ArgumentResolver(); + $factory = new ArgumentMetadataFactory(); + $argumentValueResolvers = array( + new ArgumentFromAttributeResolver(), + new VariadicArgumentValueResolver(), + new RequestResolver(), + new DefaultArgumentValueResolver(), + ); + + $resolver = new ArgumentResolver($factory, $argumentValueResolvers); $request = Request::create('/'); $controller = array(new self(), 'testGetArguments'); @@ -62,7 +72,7 @@ public function testGetArguments() $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('foobar', 'foobar'); - $controller = 'Symfony\Component\HttpKernel\Tests\Controller\another_controller_function'; + $controller = 'Symfony\Component\HttpKernel\Tests\Controller\argument_resolver_controller_function'; $this->assertEquals(array('foo', 'foobar'), $resolver->getArguments($request, $controller)); $request = Request::create('/'); @@ -84,11 +94,18 @@ public function testGetArguments() /** * @requires PHP 5.6 - * @group legacy */ public function testGetVariadicArguments() { - $resolver = new ControllerResolver(); + $factory = new ArgumentMetadataFactory(); + $argumentValueResolvers = array( + new ArgumentFromAttributeResolver(), + new VariadicArgumentValueResolver(), + new RequestResolver(), + new DefaultArgumentValueResolver(), + ); + + $resolver = new ArgumentResolver($factory, $argumentValueResolvers); $request = Request::create('/'); $request->attributes->set('foo', 'foo'); @@ -97,14 +114,47 @@ public function testGetVariadicArguments() $this->assertEquals(array('foo', 'foo', 'bar'), $resolver->getArguments($request, $controller)); } - public function testCreateControllerCanReturnAnyCallable() + /** + * @requires PHP 5.6 + * @expectedException \InvalidArgumentException + */ + public function testGetVariadicArgumentsWithoutArrayInRequest() + { + $factory = new ArgumentMetadataFactory(); + $argumentValueResolvers = array( + new ArgumentFromAttributeResolver(), + new VariadicArgumentValueResolver(), + new RequestResolver(), + new DefaultArgumentValueResolver(), + ); + + $resolver = new ArgumentResolver($factory, $argumentValueResolvers); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', 'foo'); + $controller = array(new VariadicController(), 'action'); + $resolver->getArguments($request, $controller); + } + + /** + * @requires PHP 5.6 + * @expectedException \InvalidArgumentException + */ + public function testGetArgumentWithoutArray() { - $mock = $this->getMock('Symfony\Component\HttpKernel\Controller\ControllerResolver', array('createController')); - $mock->expects($this->once())->method('createController')->will($this->returnValue('Symfony\Component\HttpKernel\Tests\Controller\another_controller_function')); + $factory = new ArgumentMetadataFactory(); + $valueResolver = $this->getMock(ArgumentValueResolverInterface::class); + $resolver = new ArgumentResolver($factory, array($valueResolver)); + + $valueResolver->expects($this->any())->method('supports')->willReturn(true); + $valueResolver->expects($this->any())->method('resolve')->willReturn('foo'); $request = Request::create('/'); - $request->attributes->set('_controller', 'foobar'); - $mock->getController($request); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', 'foo'); + $controller = array($this, 'controllerMethod2'); + $resolver->getArguments($request, $controller); } public function __invoke($foo, $bar = null) @@ -132,6 +182,6 @@ protected function controllerMethod5(Request $request) } } -function another_controller_function($foo, $foobar) +function argument_resolver_controller_function($foo, $foobar) { } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/LegacyArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/LegacyArgumentResolverTest.php new file mode 100644 index 0000000000000..f1632594a8a81 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/LegacyArgumentResolverTest.php @@ -0,0 +1,123 @@ + + * + * 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 Symfony\Component\HttpKernel\Controller\LegacyArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ControllerResolver; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; +use Symfony\Component\HttpFoundation\Request; + +class LegacyArgumentResolverTest extends \PHPUnit_Framework_TestCase +{ + public function testGetArguments() + { + $resolver = new LegacyArgumentResolver(); + + $request = Request::create('/'); + $controller = array(new self(), 'testGetArguments'); + $this->assertEquals(array(), $resolver->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments'); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = array(new self(), 'controllerMethod1'); + $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method'); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = array(new self(), 'controllerMethod2'); + $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller), '->getArguments() uses default values if present'); + + $request->attributes->set('bar', 'bar'); + $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes'); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = function ($foo) {}; + $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = function ($foo, $bar = 'bar') {}; + $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $controller = new self(); + $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller)); + $request->attributes->set('bar', 'bar'); + $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('foobar', 'foobar'); + $controller = 'Symfony\Component\HttpKernel\Tests\Controller\another_controller_function'; + $this->assertEquals(array('foo', 'foobar'), $resolver->getArguments($request, $controller)); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('foobar', 'foobar'); + $controller = array(new self(), 'controllerMethod3'); + + try { + $resolver->getArguments($request, $controller); + $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + } catch (\Exception $e) { + $this->assertInstanceOf('\RuntimeException', $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); + } + + $request = Request::create('/'); + $controller = array(new self(), 'controllerMethod5'); + $this->assertEquals(array($request), $resolver->getArguments($request, $controller), '->getArguments() injects the request'); + } + + /** + * @requires PHP 5.6 + */ + public function testGetVariadicArguments() + { + $resolver = new ControllerResolver(); + + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); + $request->attributes->set('bar', array('foo', 'bar')); + $controller = array(new VariadicController(), 'action'); + $this->assertEquals(array('foo', 'foo', 'bar'), $resolver->getArguments($request, $controller)); + } + + public function __invoke($foo, $bar = null) + { + } + + public function controllerMethod1($foo) + { + } + + protected function controllerMethod2($foo, $bar = null) + { + } + + protected function controllerMethod3($foo, $bar, $foobar) + { + } + + protected static function controllerMethod4() + { + } + + protected function controllerMethod5(Request $request) + { + } +} + +function another_controller_function($foo, $foobar) +{ +} diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php new file mode 100644 index 0000000000000..bf57504fdd184 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php @@ -0,0 +1,133 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata; + +use Fake\ImportedAndFake; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; + +class ArgumentMetadataFactoryTest extends \PHPUnit_Framework_TestCase +{ + private $factory; + + protected function setUp() + { + $this->factory = new ArgumentMetadataFactory(); + } + + public function testSignature1() + { + $arguments = $this->factory->createArgumentMetadata([$this, 'signature1']); + + $this->assertEquals(array( + new ArgumentMetadata('foo', self::class, false, false, null), + new ArgumentMetadata('bar', 'array', false, false, null), + new ArgumentMetadata('baz', 'callable', false, false, null), + ), $arguments); + } + + public function testSignature2() + { + $arguments = $this->factory->createArgumentMetadata([$this, 'signature2']); + + $this->assertEquals(array( + new ArgumentMetadata('foo', self::class, false, true, null), + new ArgumentMetadata('bar', __NAMESPACE__.'\FakeClassThatDoesNotExist', false, true, null), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, true, null), + ), $arguments); + } + + public function testSignature3() + { + $arguments = $this->factory->createArgumentMetadata([$this, 'signature3']); + + $this->assertEquals(array( + new ArgumentMetadata('bar', __NAMESPACE__.'\FakeClassThatDoesNotExist', false, false, null), + new ArgumentMetadata('baz', 'Fake\ImportedAndFake', false, false, null), + ), $arguments); + } + + public function testSignature4() + { + $arguments = $this->factory->createArgumentMetadata([$this, 'signature4']); + + $this->assertEquals(array( + new ArgumentMetadata('foo', null, false, true, 'default'), + new ArgumentMetadata('bar', null, false, true, 500), + new ArgumentMetadata('baz', null, false, true, []), + ), $arguments); + } + + public function testSignature5() + { + $arguments = $this->factory->createArgumentMetadata([$this, 'signature5']); + + $this->assertEquals(array( + new ArgumentMetadata('foo', 'array', false, true, null), + new ArgumentMetadata('bar', null, false, false, null), + ), $arguments); + } + + /** + * @requires PHP 5.6 + */ + public function testVariadicSignature() + { + $arguments = $this->factory->createArgumentMetadata([new VariadicController(), 'action']); + + $this->assertEquals(array( + new ArgumentMetadata('foo', null, false, false, null), + new ArgumentMetadata('bar', null, true, false, null), + ), $arguments); + } + + /** + * @requires PHP 7.0 + */ + public function testBasicTypesSignature() + { + $arguments = $this->factory->createArgumentMetadata([new BasicTypesController(), 'action']); + + $this->assertEquals(array( + new ArgumentMetadata('foo', 'string', false, false, null), + new ArgumentMetadata('bar', 'int', false, false, null), + new ArgumentMetadata('baz', 'float', false, false, null), + ), $arguments); + } + + private function signature1(ArgumentMetadataFactoryTest $foo, array $bar, callable $baz) + { + + } + + private function signature2(ArgumentMetadataFactoryTest $foo = null, FakeClassThatDoesNotExist $bar = null, ImportedAndFake $baz = null) + { + + } + + private function signature3(FakeClassThatDoesNotExist $bar, ImportedAndFake $baz) + { + + } + + private function signature4($foo = 'default', $bar = 500, $baz = []) + { + + } + + private function signature5(array $foo = null, $bar) + { + + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php index 4234b90832d6a..01788b1d79228 100644 --- a/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/DataCollector/RequestDataCollectorTest.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\Tests\DataCollector; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; @@ -191,7 +192,7 @@ protected function createResponse() protected function injectController($collector, $controller, $request) { $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $httpKernel = new HttpKernel(new EventDispatcher(), $resolver); + $httpKernel = new HttpKernel(new EventDispatcher(), $resolver, null, $this->getMock(ArgumentResolverInterface::class)); $event = new FilterControllerEvent($httpKernel, $controller, $request, HttpKernelInterface::MASTER_REQUEST); $collector->onKernelController($event); } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/BasicTypesController.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/BasicTypesController.php new file mode 100644 index 0000000000000..1a603c2c08052 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/BasicTypesController.php @@ -0,0 +1,10 @@ +render(new ControllerReference('main_controller', array('object' => $object), array()), Request::create('/')); } - public function testRenderWithObjectsAsAttributesPassedAsObjectsInTheController() + /** + * @group legacy + */ + public function testRenderWithObjectsAsAttributesPassedAsObjectsInTheControllerLegacy() { $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver', array('getController')); $resolver @@ -62,7 +71,34 @@ public function testRenderWithObjectsAsAttributesPassedAsObjectsInTheController( })) ; - $kernel = new HttpKernel(new EventDispatcher(), $resolver, new RequestStack(), new ArgumentResolver()); + $kernel = new HttpKernel(new EventDispatcher(), $resolver, new RequestStack()); + $renderer = new InlineFragmentRenderer($kernel); + + $response = $renderer->render(new ControllerReference('main_controller', array('object' => new \stdClass(), 'object1' => new Bar()), array()), Request::create('/')); + $this->assertEquals('bar', $response->getContent()); + } + + /** + * @group legacy + */ + public function testRenderWithObjectsAsAttributesPassedAsObjectsInTheController() + { + $resolver = $this->getMock(ControllerResolverInterface::class); + $resolver + ->expects($this->once()) + ->method('getController') + ->will($this->returnValue(function (\stdClass $object, Bar $object1) { + return new Response($object1->getBar()); + })) + ; + $argumentValueResolvers = array( + new ArgumentFromAttributeResolver(), + new VariadicArgumentValueResolver(), + new RequestResolver(), + new DefaultArgumentValueResolver(), + ); + + $kernel = new HttpKernel(new EventDispatcher(), $resolver, new RequestStack(), new ArgumentResolver(new ArgumentMetadataFactory(), $argumentValueResolvers)); $renderer = new InlineFragmentRenderer($kernel); $response = $renderer->render(new ControllerReference('main_controller', array('object' => new \stdClass(), 'object1' => new Bar()), array()), Request::create('/')); diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php index 5546ba2ed830e..946c7a31cb44b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestHttpKernel.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\Tests\HttpCache; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; @@ -18,7 +19,7 @@ use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\EventDispatcher\EventDispatcher; -class TestHttpKernel extends HttpKernel implements ControllerResolverInterface +class TestHttpKernel extends HttpKernel implements ControllerResolverInterface, ArgumentResolverInterface { protected $body; protected $status; @@ -35,7 +36,7 @@ public function __construct($body, $status, $headers, \Closure $customizer = nul $this->headers = $headers; $this->customizer = $customizer; - parent::__construct(new EventDispatcher(), $this); + parent::__construct(new EventDispatcher(), $this, null, $this); } public function getBackendRequest() diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php index 5b5209e9a678f..926d8daf53115 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/TestMultipleHttpKernel.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel\Tests\HttpCache; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpFoundation\Request; @@ -18,7 +19,7 @@ use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\EventDispatcher\EventDispatcher; -class TestMultipleHttpKernel extends HttpKernel implements ControllerResolverInterface +class TestMultipleHttpKernel extends HttpKernel implements ControllerResolverInterface, ArgumentResolverInterface { protected $bodies = array(); protected $statuses = array(); @@ -34,7 +35,7 @@ public function __construct($responses) $this->headers[] = $response['headers']; } - parent::__construct(new EventDispatcher(), $this); + parent::__construct(new EventDispatcher(), $this, null, $this); } public function getBackendRequest() diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php index 06f611ef464fd..60c2a177793ae 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpKernelTest.php @@ -11,6 +11,10 @@ namespace Symfony\Component\HttpKernel\Tests; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; +use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpKernel\HttpKernelInterface; use Symfony\Component\HttpKernel\KernelEvents; @@ -28,7 +32,7 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase */ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrue() { - $kernel = new HttpKernel(new EventDispatcher(), $this->getResolver(function () { throw new \RuntimeException(); })); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }); $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); } @@ -38,7 +42,7 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrue() */ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsFalseAndNoListenerIsRegistered() { - $kernel = new HttpKernel(new EventDispatcher(), $this->getResolver(function () { throw new \RuntimeException(); })); + $kernel = $this->getHttpKernel(new EventDispatcher(), function () { throw new \RuntimeException(); }); $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, false); } @@ -50,7 +54,7 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithAHand $event->setResponse(new Response($event->getException()->getMessage())); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () { throw new \RuntimeException('foo'); })); + $kernel = $this->getHttpKernel($dispatcher, function () { throw new \RuntimeException('foo'); }); $response = $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); $this->assertEquals('500', $response->getStatusCode()); @@ -66,7 +70,7 @@ public function testHandleWhenControllerThrowsAnExceptionAndCatchIsTrueWithANonH // should set a response, but does not }); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () use ($exception) { throw $exception; })); + $kernel = $this->getHttpKernel($dispatcher, function () use ($exception) { throw $exception; }); try { $kernel->handle(new Request(), HttpKernelInterface::MASTER_REQUEST, true); @@ -83,7 +87,7 @@ public function testHandleExceptionWithARedirectionResponse() $event->setResponse(new RedirectResponse('/login', 301)); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () { throw new AccessDeniedHttpException(); })); + $kernel = $this->getHttpKernel($dispatcher, function () { throw new AccessDeniedHttpException(); }); $response = $kernel->handle(new Request()); $this->assertEquals('301', $response->getStatusCode()); @@ -97,7 +101,7 @@ public function testHandleHttpException() $event->setResponse(new Response($event->getException()->getMessage())); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () { throw new MethodNotAllowedHttpException(array('POST')); })); + $kernel = $this->getHttpKernel($dispatcher, function () { throw new MethodNotAllowedHttpException(array('POST')); }); $response = $kernel->handle(new Request()); $this->assertEquals('405', $response->getStatusCode()); @@ -114,7 +118,7 @@ public function testHandleWhenAnExceptionIsHandledWithASpecificStatusCode($respo $event->setResponse(new Response('', $responseStatusCode, array('X-Status-Code' => $expectedStatusCode))); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () { throw new \RuntimeException(); })); + $kernel = $this->getHttpKernel($dispatcher, function () { throw new \RuntimeException(); }); $response = $kernel->handle(new Request()); $this->assertEquals($expectedStatusCode, $response->getStatusCode()); @@ -138,7 +142,7 @@ public function testHandleWhenAListenerReturnsAResponse() $event->setResponse(new Response('hello')); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver()); + $kernel = $this->getHttpKernel($dispatcher); $this->assertEquals('hello', $kernel->handle(new Request())->getContent()); } @@ -149,7 +153,7 @@ public function testHandleWhenAListenerReturnsAResponse() public function testHandleWhenNoControllerIsFound() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(false)); + $kernel = $this->getHttpKernel($dispatcher, false); $kernel->handle(new Request()); } @@ -158,7 +162,7 @@ public function testHandleWhenTheControllerIsAClosure() { $response = new Response('foo'); $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () use ($response) { return $response; })); + $kernel = $this->getHttpKernel($dispatcher, function () use ($response) { return $response; }); $this->assertSame($response, $kernel->handle(new Request())); } @@ -166,7 +170,7 @@ public function testHandleWhenTheControllerIsAClosure() public function testHandleWhenTheControllerIsAnObjectWithInvoke() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(new Controller())); + $kernel = $this->getHttpKernel($dispatcher, new Controller()); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } @@ -174,7 +178,7 @@ public function testHandleWhenTheControllerIsAnObjectWithInvoke() public function testHandleWhenTheControllerIsAFunction() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver('Symfony\Component\HttpKernel\Tests\controller_func')); + $kernel = $this->getHttpKernel($dispatcher, 'Symfony\Component\HttpKernel\Tests\controller_func'); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } @@ -182,7 +186,7 @@ public function testHandleWhenTheControllerIsAFunction() public function testHandleWhenTheControllerIsAnArray() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(array(new Controller(), 'controller'))); + $kernel = $this->getHttpKernel($dispatcher, array(new Controller(), 'controller')); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } @@ -190,7 +194,7 @@ public function testHandleWhenTheControllerIsAnArray() public function testHandleWhenTheControllerIsAStaticArray() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(array('Symfony\Component\HttpKernel\Tests\Controller', 'staticcontroller'))); + $kernel = $this->getHttpKernel($dispatcher, array('Symfony\Component\HttpKernel\Tests\Controller', 'staticcontroller')); $this->assertResponseEquals(new Response('foo'), $kernel->handle(new Request())); } @@ -201,7 +205,7 @@ public function testHandleWhenTheControllerIsAStaticArray() public function testHandleWhenTheControllerDoesNotReturnAResponse() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () { return 'foo'; })); + $kernel = $this->getHttpKernel($dispatcher, function () { return 'foo'; }); $kernel->handle(new Request()); } @@ -212,7 +216,8 @@ public function testHandleWhenTheControllerDoesNotReturnAResponseButAViewIsRegis $dispatcher->addListener(KernelEvents::VIEW, function ($event) { $event->setResponse(new Response($event->getControllerResult())); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver(function () { return 'foo'; })); + + $kernel = $this->getHttpKernel($dispatcher, function () { return 'foo'; }); $this->assertEquals('foo', $kernel->handle(new Request())->getContent()); } @@ -223,7 +228,7 @@ public function testHandleWithAResponseListener() $dispatcher->addListener(KernelEvents::RESPONSE, function ($event) { $event->setResponse(new Response('foo')); }); - $kernel = new HttpKernel($dispatcher, $this->getResolver()); + $kernel = $this->getHttpKernel($dispatcher); $this->assertEquals('foo', $kernel->handle(new Request())->getContent()); } @@ -231,7 +236,7 @@ public function testHandleWithAResponseListener() public function testTerminate() { $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver()); + $kernel = $this->getHttpKernel($dispatcher); $dispatcher->addListener(KernelEvents::TERMINATE, function ($event) use (&$called, &$capturedKernel, &$capturedRequest, &$capturedResponse) { $called = true; $capturedKernel = $event->getKernel(); @@ -255,29 +260,33 @@ public function testVerifyRequestStackPushPopDuringHandle() $stack->expects($this->at(1))->method('pop'); $dispatcher = new EventDispatcher(); - $kernel = new HttpKernel($dispatcher, $this->getResolver(), $stack); + $kernel = $this->getHttpKernel($dispatcher, null, $stack); $kernel->handle($request, HttpKernelInterface::MASTER_REQUEST); } - protected function getResolver($controller = null) + private function getHttpKernel(EventDispatcherInterface $eventDispatcher, $controller = null, RequestStack $requestStack = null) { if (null === $controller) { $controller = function () { return new Response('Hello'); }; } - $resolver = $this->getMock('Symfony\\Component\\HttpKernel\\Controller\\ControllerResolverInterface'); - $resolver->expects($this->any()) + $controllerResolver = $this->getMock(ControllerResolverInterface::class); + $controllerResolver + ->expects($this->any()) ->method('getController') ->will($this->returnValue($controller)); - $resolver->expects($this->any()) + + $argumentResolver = $this->getMock(ArgumentResolverInterface::class); + $argumentResolver + ->expects($this->any()) ->method('getArguments') ->will($this->returnValue(array())); - return $resolver; + return new HttpKernel($eventDispatcher, $controllerResolver, $requestStack, $argumentResolver); } - protected function assertResponseEquals(Response $expected, Response $actual) + private function assertResponseEquals(Response $expected, Response $actual) { $expected->setDate($actual->getDate()); $this->assertEquals($expected, $actual); diff --git a/src/Symfony/Component/HttpKernel/Tests/TestHttpKernel.php b/src/Symfony/Component/HttpKernel/Tests/TestHttpKernel.php index d526c4de80c36..3ec59272541a4 100644 --- a/src/Symfony/Component/HttpKernel/Tests/TestHttpKernel.php +++ b/src/Symfony/Component/HttpKernel/Tests/TestHttpKernel.php @@ -11,17 +11,18 @@ namespace Symfony\Component\HttpKernel\Tests; +use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\EventDispatcher\EventDispatcher; -class TestHttpKernel extends HttpKernel implements ControllerResolverInterface +class TestHttpKernel extends HttpKernel implements ControllerResolverInterface, ArgumentResolverInterface { public function __construct() { - parent::__construct(new EventDispatcher(), $this); + parent::__construct(new EventDispatcher(), $this, null, $this); } public function getController(Request $request) From cee5106960c38fe70585c1c9e9634a663f6f5456 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Fri, 1 Apr 2016 06:28:01 +0200 Subject: [PATCH 3/5] cs fixes --- UPGRADE-3.1.md | 9 ++++----- .../Bundle/FrameworkBundle/Resources/config/web.xml | 4 ++-- src/Symfony/Component/HttpKernel/CHANGELOG.md | 10 ++++++---- .../HttpKernel/Controller/ArgumentResolver.php | 2 +- .../Controller/ArgumentResolverInterface.php | 4 ++-- .../ArgumentFromAttributeResolver.php | 2 +- .../DefaultArgumentValueResolver.php | 2 +- .../ArgumentValueResolver/RequestResolver.php | 4 ++-- .../VariadicArgumentValueResolver.php | 2 +- .../Controller/ArgumentValueResolverInterface.php | 4 +--- .../HttpKernel/Controller/ControllerResolver.php | 4 ++-- .../HttpKernel/Controller/LegacyArgumentResolver.php | 2 +- .../Controller/TraceableControllerResolver.php | 2 +- .../ControllerMetadata/ArgumentMetadataFactory.php | 8 ++++---- 14 files changed, 29 insertions(+), 30 deletions(-) diff --git a/UPGRADE-3.1.md b/UPGRADE-3.1.md index b3de5e123a88e..6518f4f8fb24d 100644 --- a/UPGRADE-3.1.md +++ b/UPGRADE-3.1.md @@ -66,11 +66,10 @@ HttpKernel deprecated and will be removed in Symfony 4.0. The inline fragment renderer should be used with object attributes. - * The `ControllerResolver::getArguments()` method is deprecated and will be - removed in 4.0. If you have your own `ControllerResolverInterface` - implementation, you should replace this method by implementing the - `ArgumentResolverInterface` and injecting it in the `HttpKernel`, or using - the `ArgumentResolver` and injecting this in the `HttpKernel`. + * The `ControllerResolver::getArguments()` method has been deprecated and will + be removed in 4.0. If you have your own `ControllerResolverInterface` + implementation, you should inject either an `ArgumentResolverInterface` + instance or the new `ArgumentResolver` in the `HttpKernel`. Serializer ---------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 4abcdc4377028..3b6a37f10c968 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -17,13 +17,13 @@ + + - - diff --git a/src/Symfony/Component/HttpKernel/CHANGELOG.md b/src/Symfony/Component/HttpKernel/CHANGELOG.md index d430cb1b13e19..dc42e4a139755 100644 --- a/src/Symfony/Component/HttpKernel/CHANGELOG.md +++ b/src/Symfony/Component/HttpKernel/CHANGELOG.md @@ -4,10 +4,12 @@ CHANGELOG 3.1.0 ----- * deprecated passing objects as URI attributes to the ESI and SSI renderers - * Added a `LegacyArgumentResolver` with `getArguments()` and the corresponding interface `ArgumentResolverInterface` - * Deprecated `ControllerResolver::getArguments()`, which uses the `LegacyArgumentResolver` as BC layer by extending it - * The `HttpKernel` now accepts an additional argument for an `ArgumentResolverInterface` - * Added the `ArgumentResolver` which features an extension point to resolve arguments in a more dynamic way + * added `Symfony\Component\HttpKernel\Controller\LegacyArgumentResolver` + * deprecated `ControllerResolver::getArguments()` + * made `ControllerResolver` extend the `LegacyArgumentResolver` for BC + * added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface` + * added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface` as argument to `HttpKernel` + * added `Symfony\Component\HttpKernel\Controller\ArgumentResolver` 3.0.0 ----- diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 673ab90c68016..2daa1e8a6f2dd 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -15,7 +15,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; /** - * Responsible for the resolving of arguments passed to an action. + * Responsible for resolving the arguments passed to an action. * * @author Iltar van der Berg */ diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php index dce129ee7ffa3..5c512309662d7 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\Request; /** - * An ArgumentResolverInterface implementation knows how to determine the + * An ArgumentResolverInterface instance knows how to determine the * arguments for a specific action. * * @author Fabien Potencier @@ -29,7 +29,7 @@ interface ArgumentResolverInterface * * @return array An array of arguments to pass to the controller * - * @throws \RuntimeException When value for argument given is not provided + * @throws \RuntimeException When no value could be provided for a required argument */ public function getArguments(Request $request, $controller); } diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php index 0234db0e7b24c..55d87df122c22 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php @@ -16,7 +16,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** - * Grabs a non-variadic value from the request and returns it. + * Yields a non-variadic argument's value from the request attributes. * * @author Iltar van der Berg */ diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php index 36db486e05962..5c9893f3b2514 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php @@ -16,7 +16,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** - * Returns the default value defined in the action signature if present and no value has been given. + * Yields the default value defined in the action signature when no value has been given. * * @author Iltar van der Berg */ diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php index f7eff444b41dd..364afb57509f3 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php @@ -16,7 +16,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** - * Supports the same instance as the request object passed along. + * Yields the same instance as the request object passed along. * * @author Iltar van der Berg */ @@ -27,7 +27,7 @@ final class RequestResolver implements ArgumentValueResolverInterface */ public function supports(Request $request, ArgumentMetadata $argument) { - return $argument->getType() === Request::class || is_subclass_of($request, $argument->getType()); + return $argument->getType() === Request::class || is_subclass_of(Request::class, $argument->getType()); } /** diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php index 3cb302c515519..4e6f7477f22af 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php @@ -16,7 +16,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; /** - * Grabs the variadic value from the request and returns it. + * Yields a variadic argument's values from the request attributes. * * @author Iltar van der Berg */ diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php index 278fe4754dc5f..ccaada171568e 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolverInterface.php @@ -22,7 +22,7 @@ interface ArgumentValueResolverInterface { /** - * Whether this resolver can resolve can resolve the value for the given ArgumentMetadata. + * Whether this resolver can resolve the value for the given ArgumentMetadata. * * @param Request $request * @param ArgumentMetadata $argument @@ -34,8 +34,6 @@ public function supports(Request $request, ArgumentMetadata $argument); /** * Yield the possible value(s). * - * An implementation must yield at least one value. - * * @param Request $request * @param ArgumentMetadata $argument * diff --git a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php index 13467d30e516b..0a9ed8075c13d 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ControllerResolver.php @@ -85,7 +85,7 @@ public function getController(Request $request) /** * {@inheritdoc} * - * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead. + * @deprecated This method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead. */ public function getArguments(Request $request, $controller) { @@ -95,7 +95,7 @@ public function getArguments(Request $request, $controller) } /** - * @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead. + * @deprecated This method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead. */ protected function doGetArguments(Request $request, $controller, array $parameters) { diff --git a/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php index 22ee655152894..258fa9811a71d 100644 --- a/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/LegacyArgumentResolver.php @@ -27,7 +27,7 @@ class LegacyArgumentResolver implements ArgumentResolverInterface */ public function getArguments(Request $request, $controller) { - // only trigger the deprecation notice if actually used, the ControllerResolver still extends this for BC reasons + // only trigger the deprecation notice if actually used, the ControllerResolver still extends it for BC @trigger_error(sprintf('The %s class is deprecated since 3.1 and will be removed in 4.0. Please use the %s instead.', __CLASS__, ArgumentResolver::class), E_USER_DEPRECATED); if (is_array($controller)) { diff --git a/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php b/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php index 78571fb0c82fd..d65b28fb4133c 100644 --- a/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/TraceableControllerResolver.php @@ -38,7 +38,7 @@ public function __construct(ControllerResolverInterface $resolver, Stopwatch $st $this->stopwatch = $stopwatch; $this->argumentResolver = $argumentResolver; - // required for BC reasons + // BC if (null === $this->argumentResolver) { $this->argumentResolver = $resolver; } diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php index 0c669806127da..bf987b199846c 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php @@ -34,7 +34,7 @@ public function createArgumentMetadata($controller) } foreach ($reflection->getParameters() as $param) { - $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $this->isVariadic($param), $this->hasDefaulValue($param), $this->getDefaulValue($param)); + $arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param), $this->isVariadic($param), $this->hasDefaultValue($param), $this->getDefaultValue($param)); } return $arguments; @@ -59,7 +59,7 @@ private function isVariadic(\ReflectionParameter $parameter) * * @return bool */ - private function hasDefaulValue(\ReflectionParameter $parameter) + private function hasDefaultValue(\ReflectionParameter $parameter) { return $parameter->isDefaultValueAvailable(); } @@ -71,9 +71,9 @@ private function hasDefaulValue(\ReflectionParameter $parameter) * * @return mixed|null */ - private function getDefaulValue(\ReflectionParameter $parameter) + private function getDefaultValue(\ReflectionParameter $parameter) { - return $this->hasDefaulValue($parameter) ? $parameter->getDefaultValue() : null; + return $this->hasDefaultValue($parameter) ? $parameter->getDefaultValue() : null; } /** From f29bf4cd4a58e46827a1076056b502ad28398f77 Mon Sep 17 00:00:00 2001 From: Jules Pietri Date: Fri, 1 Apr 2016 07:14:26 +0200 Subject: [PATCH 4/5] Refactor ArgumentResolverTest --- .../Tests/Controller/ArgumentResolverTest.php | 128 +++++++++++------- 1 file changed, 79 insertions(+), 49 deletions(-) diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php index 18916f56972c3..4d070ecd5151b 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php @@ -23,7 +23,10 @@ class ArgumentResolverTest extends \PHPUnit_Framework_TestCase { - public function testGetArguments() + /** @var ArgumentResolver */ + private static $resolver; + + public static function setUpBeforeClass() { $factory = new ArgumentMetadataFactory(); $argumentValueResolvers = array( @@ -33,63 +36,108 @@ public function testGetArguments() new DefaultArgumentValueResolver(), ); - $resolver = new ArgumentResolver($factory, $argumentValueResolvers); + self::$resolver = new ArgumentResolver($factory, $argumentValueResolvers); + } + public function testGetArguments() + { $request = Request::create('/'); - $controller = array(new self(), 'testGetArguments'); - $this->assertEquals(array(), $resolver->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments'); + $request->attributes->set('foo', 'foo'); + $controller = array(new self(), 'controllerWithFoo'); + + $this->assertEquals(array('foo'), self::$resolver->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method'); + } + public function testGetArgumentsReturnsEmptyArrayWhenNoArguments() + { $request = Request::create('/'); - $request->attributes->set('foo', 'foo'); - $controller = array(new self(), 'controllerMethod1'); - $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller), '->getArguments() returns an array of arguments for the controller method'); + $controller = array(new self(), 'controllerWithoutArguments'); + + $this->assertEquals(array(), self::$resolver->getArguments($request, $controller), '->getArguments() returns an empty array if the method takes no arguments'); + } + public function testGetArgumentsUsesDefaultValue() + { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); - $controller = array(new self(), 'controllerMethod2'); - $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller), '->getArguments() uses default values if present'); + $controller = array(new self(), 'controllerWithFooAndDefaultBar'); + + $this->assertEquals(array('foo', null), self::$resolver->getArguments($request, $controller), '->getArguments() uses default values if present'); + } + public function testGetArgumentsOverrideDefaultValueByRequestAttribute() + { + $request = Request::create('/'); + $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'bar'); - $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes'); + $controller = array(new self(), 'controllerWithFooAndDefaultBar'); + + $this->assertEquals(array('foo', 'bar'), self::$resolver->getArguments($request, $controller), '->getArguments() overrides default values if provided in the request attributes'); + } + public function testGetArgumentsFromClosure() + { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = function ($foo) {}; - $this->assertEquals(array('foo'), $resolver->getArguments($request, $controller)); + $this->assertEquals(array('foo'), self::$resolver->getArguments($request, $controller)); + } + + public function testGetArgumentsUsesDefaultValueFromClosure() + { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = function ($foo, $bar = 'bar') {}; - $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller)); + $this->assertEquals(array('foo', 'bar'), self::$resolver->getArguments($request, $controller)); + } + + public function testGetArgumentsFromInvokableObject() + { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $controller = new self(); - $this->assertEquals(array('foo', null), $resolver->getArguments($request, $controller)); + + $this->assertEquals(array('foo', null), self::$resolver->getArguments($request, $controller)); + + // Test default bar overridden by request attribute $request->attributes->set('bar', 'bar'); - $this->assertEquals(array('foo', 'bar'), $resolver->getArguments($request, $controller)); + $this->assertEquals(array('foo', 'bar'), self::$resolver->getArguments($request, $controller)); + } + + public function testGetArgumentsFromFunctionName() + { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('foobar', 'foobar'); - $controller = 'Symfony\Component\HttpKernel\Tests\Controller\argument_resolver_controller_function'; - $this->assertEquals(array('foo', 'foobar'), $resolver->getArguments($request, $controller)); + $controller = __NAMESPACE__.'\controller_function'; + + $this->assertEquals(array('foo', 'foobar'), self::$resolver->getArguments($request, $controller)); + } + public function testGetArgumentsFailsOnUnresolvedValue() + { $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('foobar', 'foobar'); - $controller = array(new self(), 'controllerMethod3'); + $controller = array(new self(), 'controllerWithFooBarFoobar'); try { - $resolver->getArguments($request, $controller); + self::$resolver->getArguments($request, $controller); $this->fail('->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); } catch (\Exception $e) { $this->assertInstanceOf('\RuntimeException', $e, '->getArguments() throws a \RuntimeException exception if it cannot determine the argument value'); } + } + public function testGetArgumentsInjectsRequest() + { $request = Request::create('/'); - $controller = array(new self(), 'controllerMethod5'); - $this->assertEquals(array($request), $resolver->getArguments($request, $controller), '->getArguments() injects the request'); + $controller = array(new self(), 'controllerWithRequest'); + + $this->assertEquals(array($request), self::$resolver->getArguments($request, $controller), '->getArguments() injects the request'); } /** @@ -97,21 +145,12 @@ public function testGetArguments() */ public function testGetVariadicArguments() { - $factory = new ArgumentMetadataFactory(); - $argumentValueResolvers = array( - new ArgumentFromAttributeResolver(), - new VariadicArgumentValueResolver(), - new RequestResolver(), - new DefaultArgumentValueResolver(), - ); - - $resolver = new ArgumentResolver($factory, $argumentValueResolvers); - $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', array('foo', 'bar')); $controller = array(new VariadicController(), 'action'); - $this->assertEquals(array('foo', 'foo', 'bar'), $resolver->getArguments($request, $controller)); + + $this->assertEquals(array('foo', 'foo', 'bar'), self::$resolver->getArguments($request, $controller)); } /** @@ -120,21 +159,12 @@ public function testGetVariadicArguments() */ public function testGetVariadicArgumentsWithoutArrayInRequest() { - $factory = new ArgumentMetadataFactory(); - $argumentValueResolvers = array( - new ArgumentFromAttributeResolver(), - new VariadicArgumentValueResolver(), - new RequestResolver(), - new DefaultArgumentValueResolver(), - ); - - $resolver = new ArgumentResolver($factory, $argumentValueResolvers); - $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'foo'); $controller = array(new VariadicController(), 'action'); - $resolver->getArguments($request, $controller); + + self::$resolver->getArguments($request, $controller); } /** @@ -153,7 +183,7 @@ public function testGetArgumentWithoutArray() $request = Request::create('/'); $request->attributes->set('foo', 'foo'); $request->attributes->set('bar', 'foo'); - $controller = array($this, 'controllerMethod2'); + $controller = array($this, 'controllerWithFooAndDefaultBar'); $resolver->getArguments($request, $controller); } @@ -161,27 +191,27 @@ public function __invoke($foo, $bar = null) { } - public function controllerMethod1($foo) + public function controllerWithFoo($foo) { } - protected function controllerMethod2($foo, $bar = null) + public function controllerWithoutArguments() { } - protected function controllerMethod3($foo, $bar, $foobar) + protected function controllerWithFooAndDefaultBar($foo, $bar = null) { } - protected static function controllerMethod4() + protected function controllerWithFooBarFoobar($foo, $bar, $foobar) { } - protected function controllerMethod5(Request $request) + protected function controllerWithRequest(Request $request) { } } -function argument_resolver_controller_function($foo, $foobar) +function controller_function($foo, $foobar) { } From 1bf80c92ee3daf4c526189f82a3dfa7a0482bc44 Mon Sep 17 00:00:00 2001 From: Iltar van der Berg Date: Fri, 1 Apr 2016 08:34:22 +0200 Subject: [PATCH 5/5] Improved DX for the ArgumentResolver --- .../FrameworkBundle/Resources/config/web.xml | 8 ++--- .../Controller/ArgumentResolver.php | 14 ++++++-- .../DefaultValueResolver.php} | 4 +-- .../RequestAttributeValueResolver.php} | 4 +-- .../RequestValueResolver.php} | 6 ++-- .../VariadicValueResolver.php} | 4 +-- .../ControllerMetadata/ArgumentMetadata.php | 6 +++- .../Component/HttpKernel/HttpKernel.php | 3 +- .../Tests/Controller/ArgumentResolverTest.php | 35 +++++++++++++----- .../ArgumentMetadataTest.php | 36 +++++++++++++++++++ .../Fixtures/Controller/ExtendingRequest.php | 9 +++++ .../Fragment/InlineFragmentRendererTest.php | 15 ++------ 12 files changed, 106 insertions(+), 38 deletions(-) rename src/Symfony/Component/HttpKernel/Controller/{ArgumentValueResolver/DefaultArgumentValueResolver.php => ArgumentResolver/DefaultValueResolver.php} (86%) rename src/Symfony/Component/HttpKernel/Controller/{ArgumentValueResolver/ArgumentFromAttributeResolver.php => ArgumentResolver/RequestAttributeValueResolver.php} (87%) rename src/Symfony/Component/HttpKernel/Controller/{ArgumentValueResolver/RequestResolver.php => ArgumentResolver/RequestValueResolver.php} (82%) rename src/Symfony/Component/HttpKernel/Controller/{ArgumentValueResolver/VariadicArgumentValueResolver.php => ArgumentResolver/VariadicValueResolver.php} (89%) create mode 100644 src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php create mode 100644 src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ExtendingRequest.php diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 3b6a37f10c968..932a6a11744d1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -24,19 +24,19 @@ - + - + - + - + diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php index 2daa1e8a6f2dd..1d8a0410ebbc1 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php @@ -12,6 +12,11 @@ namespace Symfony\Component\HttpKernel\Controller; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface; /** @@ -30,8 +35,13 @@ final class ArgumentResolver implements ArgumentResolverInterface public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array()) { - $this->argumentMetadataFactory = $argumentMetadataFactory; - $this->argumentValueResolvers = $argumentValueResolvers; + $this->argumentMetadataFactory = $argumentMetadataFactory ?: new ArgumentMetadataFactory(); + $this->argumentValueResolvers = $argumentValueResolvers ?: array( + new RequestAttributeValueResolver(), + new RequestValueResolver(), + new DefaultValueResolver(), + new VariadicValueResolver(), + ); } /** diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php similarity index 86% rename from src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php rename to src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php index 5c9893f3b2514..7851f40e69ba3 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/DefaultArgumentValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/DefaultValueResolver.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -20,7 +20,7 @@ * * @author Iltar van der Berg */ -final class DefaultArgumentValueResolver implements ArgumentValueResolverInterface +final class DefaultValueResolver implements ArgumentValueResolverInterface { /** * {@inheritdoc} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php similarity index 87% rename from src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php rename to src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php index 55d87df122c22..05be372d84598 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/ArgumentFromAttributeResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestAttributeValueResolver.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -20,7 +20,7 @@ * * @author Iltar van der Berg */ -final class ArgumentFromAttributeResolver implements ArgumentValueResolverInterface +final class RequestAttributeValueResolver implements ArgumentValueResolverInterface { /** * {@inheritdoc} diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php similarity index 82% rename from src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php rename to src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php index 364afb57509f3..5dabc5db401ae 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/RequestResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestValueResolver.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -20,14 +20,14 @@ * * @author Iltar van der Berg */ -final class RequestResolver implements ArgumentValueResolverInterface +final class RequestValueResolver implements ArgumentValueResolverInterface { /** * {@inheritdoc} */ public function supports(Request $request, ArgumentMetadata $argument) { - return $argument->getType() === Request::class || is_subclass_of(Request::class, $argument->getType()); + return $argument->getType() === Request::class || is_subclass_of($argument->getType(), Request::class); } /** diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php similarity index 89% rename from src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php rename to src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php index 4e6f7477f22af..56ae5f191c4d4 100644 --- a/src/Symfony/Component/HttpKernel/Controller/ArgumentValueResolver/VariadicArgumentValueResolver.php +++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/VariadicValueResolver.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver; +namespace Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; @@ -20,7 +20,7 @@ * * @author Iltar van der Berg */ -final class VariadicArgumentValueResolver implements ArgumentValueResolverInterface +final class VariadicValueResolver implements ArgumentValueResolverInterface { /** * {@inheritdoc} diff --git a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php index 71f05fab4b4ea..ca0e881fefbb3 100644 --- a/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php +++ b/src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php @@ -87,12 +87,16 @@ public function hasDefaultValue() /** * Returns the default value of the argument. * - * Make sure to call {@see self::hasDefaultValue()} first to see if a default value is possible. + * @throws \LogicException if no default value is present; {@see self::hasDefaultValue()} * * @return mixed */ public function getDefaultValue() { + if (!$this->hasDefaultValue) { + throw new \LogicException(sprintf('Argument $%s does not have a default value. Use %s::hasDefaultValue() to avoid this exception.', $this->name, __CLASS__)); + } + return $this->defaultValue; } } diff --git a/src/Symfony/Component/HttpKernel/HttpKernel.php b/src/Symfony/Component/HttpKernel/HttpKernel.php index d077623068888..0294bb584aeba 100644 --- a/src/Symfony/Component/HttpKernel/HttpKernel.php +++ b/src/Symfony/Component/HttpKernel/HttpKernel.php @@ -11,6 +11,7 @@ namespace Symfony\Component\HttpKernel; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface; use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -47,7 +48,7 @@ public function __construct(EventDispatcherInterface $dispatcher, ControllerReso $this->argumentResolver = $argumentResolver; if (null === $this->argumentResolver) { - @trigger_error(sprintf('As of 3.1 an %s is used to resolve arguments. In 4.0 the $argumentResolver becomes mandatory and the %s can no longer be used to resolve arguments.', ArgumentResolverInterface::class, ControllerResolverInterface::class), E_USER_DEPRECATED); + @trigger_error(sprintf('As of 3.1 an %s is used to resolve arguments. In 4.0 the $argumentResolver becomes the %s if no other is provided instead of using the $resolver argument.', ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED); // fallback in case of deprecations $this->argumentResolver = $resolver; } diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php index 4d070ecd5151b..3f647e0bba437 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolverTest.php @@ -12,12 +12,13 @@ namespace Symfony\Component\HttpKernel\Tests\Controller; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\ArgumentFromAttributeResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\DefaultArgumentValueResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\RequestResolver; -use Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\VariadicArgumentValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver; +use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver; use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory; +use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\ExtendingRequest; use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController; use Symfony\Component\HttpFoundation\Request; @@ -30,15 +31,21 @@ public static function setUpBeforeClass() { $factory = new ArgumentMetadataFactory(); $argumentValueResolvers = array( - new ArgumentFromAttributeResolver(), - new VariadicArgumentValueResolver(), - new RequestResolver(), - new DefaultArgumentValueResolver(), + new RequestAttributeValueResolver(), + new RequestValueResolver(), + new DefaultValueResolver(), + new VariadicValueResolver(), ); self::$resolver = new ArgumentResolver($factory, $argumentValueResolvers); } + public function testDefaultState() + { + $this->assertEquals(self::$resolver, new ArgumentResolver()); + $this->assertNotEquals(self::$resolver, new ArgumentResolver(null, array(new RequestAttributeValueResolver()))); + } + public function testGetArguments() { $request = Request::create('/'); @@ -140,6 +147,14 @@ public function testGetArgumentsInjectsRequest() $this->assertEquals(array($request), self::$resolver->getArguments($request, $controller), '->getArguments() injects the request'); } + public function testGetArgumentsInjectsExtendingRequest() + { + $request = ExtendingRequest::create('/'); + $controller = array(new self(), 'controllerWithExtendingRequest'); + + $this->assertEquals(array($request), self::$resolver->getArguments($request, $controller), '->getArguments() injects the request when extended'); + } + /** * @requires PHP 5.6 */ @@ -210,6 +225,10 @@ protected function controllerWithFooBarFoobar($foo, $bar, $foobar) protected function controllerWithRequest(Request $request) { } + + protected function controllerWithExtendingRequest(ExtendingRequest $request) + { + } } function controller_function($foo, $foobar) diff --git a/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php new file mode 100644 index 0000000000000..9713d70f8e649 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata; + +use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; + +class ArgumentMetadataTest extends \PHPUnit_Framework_TestCase +{ + public function testDefaultValueAvailable() + { + $argument = new ArgumentMetadata('foo', 'string', false, true, 'default value'); + + $this->assertTrue($argument->hasDefaultValue()); + $this->assertSame('default value', $argument->getDefaultValue()); + } + + /** + * @expectedException \LogicException + */ + public function testDefaultValueUnavailable() + { + $argument = new ArgumentMetadata('foo', 'string', false, false, null); + + $this->assertFalse($argument->hasDefaultValue()); + $argument->getDefaultValue(); + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ExtendingRequest.php b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ExtendingRequest.php new file mode 100644 index 0000000000000..e90e87c700061 --- /dev/null +++ b/src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/ExtendingRequest.php @@ -0,0 +1,9 @@ +getBar()); })) ; - $argumentValueResolvers = array( - new ArgumentFromAttributeResolver(), - new VariadicArgumentValueResolver(), - new RequestResolver(), - new DefaultArgumentValueResolver(), - ); - - $kernel = new HttpKernel(new EventDispatcher(), $resolver, new RequestStack(), new ArgumentResolver(new ArgumentMetadataFactory(), $argumentValueResolvers)); + + $kernel = new HttpKernel(new EventDispatcher(), $resolver, new RequestStack(), new ArgumentResolver()); $renderer = new InlineFragmentRenderer($kernel); $response = $renderer->render(new ControllerReference('main_controller', array('object' => new \stdClass(), 'object1' => new Bar()), array()), Request::create('/')); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy