feature #18308 Added an ArgumentResolver with clean extension point (iltar, HeahDude)

This PR was merged into the 3.1-dev branch.

Discussion
----------

Added an ArgumentResolver with clean extension point

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | yes
| Tests pass?   | yes
| Fixed tickets | #17933 (pre-work), #1547, #10710
| License       | MIT
| Doc PR        | symfony/symfony-docs#6422

**This PR is a follow up for and blocked by: #18187**, relates to #11457 by @WouterJ. When reviewing, please take the last commit: [Added an ArgumentResolver with clean extension point](4c092b31f9)

This PR provides:
- The ability to tag your own `ArgumentValueResolverInterface`. This means that you can effectively expand on the argument resolving in the `HttpKernel` without having to implement your own `ArgumentResolver`.

- The possibility to cache away argument metadata via a new `ArgumentMetadataFactory` which simply fetches the data from the cache, effectively omitting 1 reflection call per request. *Not implemented in this PR, but possible once this is merged.*

- The possibility to add a PSR-7 adapter to resolve the correct request, avoids the paramconverters
- The possibility to add a value resolver to fetch stuff from $request->query
- Drupal could simplify [their argument resolving](https://github.com/drupal/drupal/blob/8.1.x/core/lib/Drupal/Core/Controller/ControllerResolver.php) by a lot
- etc.

The aim for this PR is to provide a 100% BC variant to add argument resolving in a clean way, this is shown by the 2 tests: `LegacyArgumentResolverTest` and `ArgumentResolverTest`.

/cc @dawehner @larowlan if you have time, can you check the impact for Drupal? I think this should be a very simple change which should make it more maintainable.

Commits
-------

1bf80c9 Improved DX for the ArgumentResolver
f29bf4c Refactor ArgumentResolverTest
cee5106 cs fixes
cfcf764 Added an ArgumentResolver with clean extension point
360fc5f Extracting arg resolving from ControllerResolver
This commit is contained in:
Fabien Potencier 2016-04-03 18:53:12 +02:00
commit cef7e5b1b0
39 changed files with 1486 additions and 98 deletions

View File

@ -66,6 +66,11 @@ HttpKernel
deprecated and will be removed in Symfony 4.0. The inline fragment
renderer should be used with object attributes.
* 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
----------

View File

@ -0,0 +1,65 @@
<?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\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 <kjarli@gmail.com>
*/
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);
}
}

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,9 @@ 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\\ControllerMetadata\\ArgumentMetadata',
'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadataFactory',
'Symfony\\Component\\HttpKernel\\Event\\KernelEvent',
'Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent',
'Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent',

View File

@ -14,6 +14,7 @@ namespace Symfony\Bundle\FrameworkBundle;
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 @@ class FrameworkBundle extends Bundle
$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);

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,29 @@
<argument type="service" id="logger" on-invalid="ignore" />
</service>
<service id="argument_metadata_factory" class="Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory" public="false" />
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" public="false">
<argument type="service" id="argument_metadata_factory" />
<argument type="collection" />
</service>
<service id="argument_resolver.request_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver" public="false">
<tag name="controller_argument.value_resolver" priority="100" />
</service>
<service id="argument_resolver.request" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver" public="false">
<tag name="controller_argument.value_resolver" priority="50" />
</service>
<service id="argument_resolver.default" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver" public="false">
<tag name="controller_argument.value_resolver" priority="-100" />
</service>
<service id="argument_resolver.variadic" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver" public="false">
<tag name="controller_argument.value_resolver" priority="-150" />
</service>
<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,12 @@ CHANGELOG
3.1.0
-----
* deprecated passing objects as URI attributes to the ESI and SSI renderers
* 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
-----

View File

@ -0,0 +1,87 @@
<?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;
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;
/**
* Responsible for resolving the arguments passed to an action.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class ArgumentResolver implements ArgumentResolverInterface
{
private $argumentMetadataFactory;
/**
* @var ArgumentValueResolverInterface[]
*/
private $argumentValueResolvers;
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array())
{
$this->argumentMetadataFactory = $argumentMetadataFactory ?: new ArgumentMetadataFactory();
$this->argumentValueResolvers = $argumentValueResolvers ?: array(
new RequestAttributeValueResolver(),
new RequestValueResolver(),
new DefaultValueResolver(),
new VariadicValueResolver(),
);
}
/**
* {@inheritdoc}
*/
public function getArguments(Request $request, $controller)
{
$arguments = array();
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) {
foreach ($this->argumentValueResolvers as $resolver) {
if (!$resolver->supports($request, $metadata)) {
continue;
}
$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;
}
// 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;
}
}

View File

@ -0,0 +1,40 @@
<?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\ArgumentResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
/**
* Yields the default value defined in the action signature when no value has been given.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class DefaultValueResolver 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();
}
}

View File

@ -0,0 +1,40 @@
<?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\ArgumentResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
/**
* Yields a non-variadic argument's value from the request attributes.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class RequestAttributeValueResolver 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());
}
}

View File

@ -0,0 +1,40 @@
<?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\ArgumentResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
/**
* Yields the same instance as the request object passed along.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class RequestValueResolver implements ArgumentValueResolverInterface
{
/**
* {@inheritdoc}
*/
public function supports(Request $request, ArgumentMetadata $argument)
{
return $argument->getType() === Request::class || is_subclass_of($argument->getType(), Request::class);
}
/**
* {@inheritdoc}
*/
public function resolve(Request $request, ArgumentMetadata $argument)
{
yield $request;
}
}

View File

@ -0,0 +1,48 @@
<?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\ArgumentResolver;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
/**
* Yields a variadic argument's values from the request attributes.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
final class VariadicValueResolver 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;
}
}
}

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 instance 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
* @param callable $controller
*
* @return array An array of arguments to pass to the controller
*
* @throws \RuntimeException When no value could be provided for a required argument
*/
public function getArguments(Request $request, $controller);
}

View File

@ -0,0 +1,43 @@
<?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;
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
/**
* Responsible for resolving the value of an argument based on its metadata.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
interface ArgumentValueResolverInterface
{
/**
* Whether this resolver 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).
*
* @param Request $request
* @param ArgumentMetadata $argument
*
* @return \Generator
*/
public function resolve(Request $request, ArgumentMetadata $argument);
}

View File

@ -23,7 +23,7 @@ use Symfony\Component\HttpFoundation\Request;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class ControllerResolver implements ControllerResolverInterface
class ControllerResolver extends LegacyArgumentResolver 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 LegacyArgumentResolver 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, LegacyArgumentResolver::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 LegacyArgumentResolver 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, LegacyArgumentResolver::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,75 @@
<?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.
*
* @deprecated This class is deprecated since 3.1 and will be removed in 4.0. Please use the ArgumentResolver::class instead.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class LegacyArgumentResolver implements ArgumentResolverInterface
{
/**
* {@inheritdoc}
*/
public function getArguments(Request $request, $controller)
{
// 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)) {
$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,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,29 @@ 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;
// BC
if (null === $this->argumentResolver) {
$this->argumentResolver = $resolver;
}
}
/**
@ -52,12 +60,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

@ -0,0 +1,102 @@
<?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\ControllerMetadata;
/**
* Responsible for storing metadata of an argument.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
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.
*
* @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;
}
}

View File

@ -0,0 +1,109 @@
<?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\ControllerMetadata;
/**
* Builds {@see ArgumentMetadata} objects based on the given Controller.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
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->hasDefaultValue($param), $this->getDefaultValue($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 hasDefaultValue(\ReflectionParameter $parameter)
{
return $parameter->isDefaultValueAvailable();
}
/**
* Returns a default value if available.
*
* @param \ReflectionParameter $parameter
*
* @return mixed|null
*/
private function getDefaultValue(\ReflectionParameter $parameter)
{
return $this->hasDefaultValue($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;
}
}

View File

@ -0,0 +1,27 @@
<?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\ControllerMetadata;
/**
* Builds method argument data.
*
* @author Iltar van der Berg <kjarli@gmail.com>
*/
interface ArgumentMetadataFactoryInterface
{
/**
* @param mixed $controller The controller to resolve the arguments for.
*
* @return ArgumentMetadata[]
*/
public function createArgumentMetadata($controller);
}

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,20 @@ 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
*/
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 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;
}
}
/**
@ -133,7 +136,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,236 @@
<?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\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;
class ArgumentResolverTest extends \PHPUnit_Framework_TestCase
{
/** @var ArgumentResolver */
private static $resolver;
public static function setUpBeforeClass()
{
$factory = new ArgumentMetadataFactory();
$argumentValueResolvers = array(
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('/');
$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('/');
$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(), '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');
$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'), 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'), self::$resolver->getArguments($request, $controller));
}
public function testGetArgumentsFromInvokableObject()
{
$request = Request::create('/');
$request->attributes->set('foo', 'foo');
$controller = new self();
$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'), self::$resolver->getArguments($request, $controller));
}
public function testGetArgumentsFromFunctionName()
{
$request = Request::create('/');
$request->attributes->set('foo', 'foo');
$request->attributes->set('foobar', 'foobar');
$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(), 'controllerWithFooBarFoobar');
try {
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(), 'controllerWithRequest');
$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
*/
public function testGetVariadicArguments()
{
$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'), self::$resolver->getArguments($request, $controller));
}
/**
* @requires PHP 5.6
* @expectedException \InvalidArgumentException
*/
public function testGetVariadicArgumentsWithoutArrayInRequest()
{
$request = Request::create('/');
$request->attributes->set('foo', 'foo');
$request->attributes->set('bar', 'foo');
$controller = array(new VariadicController(), 'action');
self::$resolver->getArguments($request, $controller);
}
/**
* @requires PHP 5.6
* @expectedException \InvalidArgumentException
*/
public function testGetArgumentWithoutArray()
{
$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('foo', 'foo');
$request->attributes->set('bar', 'foo');
$controller = array($this, 'controllerWithFooAndDefaultBar');
$resolver->getArguments($request, $controller);
}
public function __invoke($foo, $bar = null)
{
}
public function controllerWithFoo($foo)
{
}
public function controllerWithoutArguments()
{
}
protected function controllerWithFooAndDefaultBar($foo, $bar = null)
{
}
protected function controllerWithFooBarFoobar($foo, $bar, $foobar)
{
}
protected function controllerWithRequest(Request $request)
{
}
protected function controllerWithExtendingRequest(ExtendingRequest $request)
{
}
}
function 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

@ -0,0 +1,123 @@
<?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\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)
{
}

View File

@ -0,0 +1,133 @@
<?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\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)
{
}
}

View File

@ -0,0 +1,36 @@
<?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\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();
}
}

View File

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

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

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller;
class BasicTypesController
{
public function action(string $foo, int $bar, float $baz)
{
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller;
use Symfony\Component\HttpFoundation\Request;
class ExtendingRequest extends Request
{
}

View File

@ -11,6 +11,9 @@
namespace Symfony\Component\HttpKernel\Tests\Fragment;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpKernel\Controller\ArgumentResolver;
use Symfony\Component\HttpKernel\Controller\ControllerResolverInterface;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
use Symfony\Component\HttpKernel\HttpKernel;
use Symfony\Component\HttpKernel\Fragment\InlineFragmentRenderer;
@ -49,7 +52,10 @@ class InlineFragmentRendererTest extends \PHPUnit_Framework_TestCase
$strategy->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
@ -60,7 +66,28 @@ class InlineFragmentRendererTest extends \PHPUnit_Framework_TestCase
}))
;
$kernel = new HttpKernel(new EventDispatcher(), $resolver);
$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());
}))
;
$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 +169,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 +179,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

View File

@ -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\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
{
protected $body;
protected $status;
@ -35,7 +36,7 @@ class TestHttpKernel extends HttpKernel implements ControllerResolverInterface
$this->headers = $headers;
$this->customizer = $customizer;
parent::__construct(new EventDispatcher(), $this);
parent::__construct(new EventDispatcher(), $this, null, $this);
}
public function getBackendRequest()

View File

@ -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\HttpFoundation\Response;
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 @@ class TestMultipleHttpKernel extends HttpKernel implements ControllerResolverInt
$this->headers[] = $response['headers'];
}
parent::__construct(new EventDispatcher(), $this);
parent::__construct(new EventDispatcher(), $this, null, $this);
}
public function getBackendRequest()

View File

@ -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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
*/
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
// 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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
{
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
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 @@ class HttpKernelTest extends \PHPUnit_Framework_TestCase
$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);

View File

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