Extracting arg resolving from ControllerResolver

This commit is contained in:
Iltar van der Berg 2016-03-16 08:56:41 +01:00
parent 8e1c60af3c
commit 360fc5fc4b
18 changed files with 373 additions and 61 deletions

View File

@ -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
----------

View File

@ -150,9 +150,6 @@ class FrameworkExtension extends Extension
$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 @@ class FrameworkExtension extends Extension
'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',

View File

@ -17,8 +17,14 @@
<argument type="service" id="logger" on-invalid="null" />
</service>
<service id="debug.controller_resolver" class="Symfony\Component\HttpKernel\Controller\TraceableControllerResolver">
<argument type="service" id="controller_resolver" />
<service id="debug.controller_resolver" decorates="controller_resolver" class="Symfony\Component\HttpKernel\Controller\TraceableControllerResolver">
<argument type="service" id="debug.controller_resolver.inner" />
<argument type="service" id="debug.stopwatch" />
<argument type="service" id="argument_resolver" />
</service>
<service id="debug.argument_resolver" decorates="argument_resolver" class="Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver">
<argument type="service" id="debug.argument_resolver.inner" />
<argument type="service" id="debug.stopwatch" />
</service>
</services>

View File

@ -13,6 +13,7 @@
<argument type="service" id="event_dispatcher" />
<argument type="service" id="controller_resolver" />
<argument type="service" id="request_stack" />
<argument type="service" id="argument_resolver" />
</service>
<service id="request_stack" class="Symfony\Component\HttpFoundation\RequestStack" />

View File

@ -17,6 +17,8 @@
<argument type="service" id="logger" on-invalid="ignore" />
</service>
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" public="false" />
<service id="response_listener" class="Symfony\Component\HttpKernel\EventListener\ResponseListener">
<tag name="kernel.event_subscriber" />
<argument>%kernel.charset%</argument>

View File

@ -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",

View File

@ -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
-----

View File

@ -0,0 +1,70 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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;
}
}

View File

@ -0,0 +1,35 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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);
}

View File

@ -23,7 +23,7 @@ use Symfony\Component\HttpFoundation\Request;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ControllerResolver implements ControllerResolverInterface
class ControllerResolver extends ArgumentResolver implements ControllerResolverInterface
{
private $logger;
@ -84,50 +84,24 @@ class ControllerResolver implements ControllerResolverInterface
/**
* {@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;
}
@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);
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;
return parent::doGetArguments($request, $controller, $parameters);
}
/**

View File

@ -52,6 +52,8 @@ interface ControllerResolverInterface
* @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);
}

View File

@ -0,0 +1,44 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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 <fabien@symfony.com>
*/
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;
}
}

View File

@ -19,21 +19,28 @@ use Symfony\Component\HttpFoundation\Request;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
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 @@ class TraceableControllerResolver implements ControllerResolverInterface
/**
* {@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();

View File

@ -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 @@ class HttpKernel implements HttpKernelInterface, TerminableInterface
$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);

View File

@ -0,0 +1,137 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* 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)
{
}

View File

@ -137,6 +137,9 @@ class ControllerResolverTest extends \PHPUnit_Framework_TestCase
);
}
/**
* @group legacy
*/
public function testGetArguments()
{
$resolver = $this->createControllerResolver();
@ -200,6 +203,7 @@ class ControllerResolverTest extends \PHPUnit_Framework_TestCase
/**
* @requires PHP 5.6
* @group legacy
*/
public function testGetVariadicArguments()
{

View File

@ -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 @@ class TraceableEventDispatcherTest extends \PHPUnit_Framework_TestCase
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);
}
}

View File

@ -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 @@ class InlineFragmentRendererTest extends \PHPUnit_Framework_TestCase
}))
;
$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 @@ class InlineFragmentRendererTest extends \PHPUnit_Framework_TestCase
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 @@ class InlineFragmentRendererTest extends \PHPUnit_Framework_TestCase
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