8000 Added an ArgumentResolver with clean extension point · symfony/symfony@cfcf764 · GitHub
[go: up one dir, main page]

Skip to content

Commit cfcf764

Browse files
author
Iltar van der Berg
committed
Added an ArgumentResolver with clean extension point
1 parent 360fc5f commit cfcf764

30 files changed

+1083
-104
lines changed

UPGRADE-3.1.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,12 @@ HttpKernel
6565
* Passing objects as URI attributes to the ESI and SSI renderers has been
6666
deprecated and will be removed in Symfony 4.0. The inline fragment
6767
renderer should be used with object attributes.
68+
6869
* The `ControllerResolver::getArguments()` method is deprecated and will be
6970
removed in 4.0. If you have your own `ControllerResolverInterface`
7071
implementation, you should replace this method by implementing the
71-
`ArgumentResolverInterface` and injecting it in the HttpKernel.
72+
`ArgumentResolverInterface` and injecting it in the `HttpKernel`, or using
73+
the `ArgumentResolver` and injecting this in the `HttpKernel`.
7274

7375
Serializer
7476
----------
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
13+
14+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
15+
use Symfony\Component\DependencyInjection\ContainerBuilder;
16+
use Symfony\Component\DependencyInjection\Reference;
17+
18+
/**
19+
* Gathers and configures the argument value resolvers.
20+
*
21+
* @author Iltar van der Berg <kjarli@gmail.com>
22+
*/
23+
class ControllerArgumentValueResolverPass implements CompilerPassInterface
24+
{
25+
public function process(ContainerBuilder $container)
26+
{
27+
if (!$container->hasDefinition('argument_resolver')) {
28+
return;
29+
}
30+
31+
$definition = $container->getDefinition('argument_resolver');
32+
$argumentResolvers = $this->findAndSortTaggedServices('controller_argument.value_resolver', $container);
33+
$definition->replaceArgument(1, $argumentResolvers);
34+
}
35+
36+
/**
37+
* Finds all services with the given tag name and order them by their priority.
38+
*
39+
* @param string $tagName
40+
* @param ContainerBuilder $container
41+
*
42+
* @return array
43+
*/
44+
private function findAndSortTaggedServices($tagName, ContainerBuilder $container)
45+
{
46+
$services = $container->findTaggedServiceIds($tagName);
47+
48+
$sortedServices = array();
49+
foreach ($services as $serviceId => $tags) {
50+
foreach ($tags as $attributes) {
51+
$priority = isset($attributes['priority']) ? $attributes['priority'] : 0;
52+
$sortedServices[$priority][] = new Reference($serviceId);
53+
}
54+
}
55+
56+
if (empty($sortedServices)) {
57+
return array();
58+
}
59+
60+
krsort($sortedServices);
61+
62+
// Flatten the array
63+
return call_user_func_array('array_merge', $sortedServices);
64+
}
65+
}

src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,8 @@ public function load(array $configs, ContainerBuilder $container)
171171
'Symfony\\Component\\HttpKernel\\EventListener\\RouterListener',
172172
'Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver',
173173
'Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver',
174+
'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata',
175+
'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadataFactory',
174176
'Symfony\\Component\\HttpKernel\\Event\\KernelEvent',
175177
'Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent',
176178
'Symfony\\Component\\HttpKernel\\Event\\FilterResponseEvent',

src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConstraintValidatorsPass;
1515
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddValidatorInitializersPass;
1616
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddConsoleCommandPass;
17+
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ControllerArgumentValueResolverPass;
1718
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\FormPass;
1819
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\PropertyInfoPass;
1920
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass;
@@ -87,6 +88,7 @@ public function build(ContainerBuilder $container)
8788
$container->addCompilerPass(new FragmentRendererPass(), PassConfig::TYPE_AFTER_REMOVING);
8889
$container->addCompilerPass(new SerializerPass());
8990
$container->addCompilerPass(new PropertyInfoPass());
91+
$container->addCompilerPass(new ControllerArgumentValueResolverPass());
9092

9193
if ($container->getParameter('kernel.debug')) {
9294
$container->addCompilerPass(new UnusedTagsPass(), PassConfig::TYPE_AFTER_REMOVING);

src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,28 @@
1717
<argument type="service" id="logger" on-invalid="ignore" />
1818
</service>
1919

20-
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" public="false" />
20+
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" public="false">
21+
<argument type="service" id="argument_metadata_factory" />
22+
<argument type="collection" />
23+
</service>
24+
25+
<service id="argument_metadata_factory" class="Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory" public="false" />
26+
27+
<service id="argument_value_resolver.argument_from_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\ArgumentFromAttributeResolver" public="false">
28+
<tag name="controller_argument.value_resolver" priority="100" />
29+
</service>
30+
31+
<service id="argument_value_resolver.argument_is_request" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\RequestResolver" public="false">
32+
<tag name="controller_argument.value_resolver" priority="50" />
33+
</service>
34+
35+
<service id="argument_value_resolver.default_argument_value" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\DefaultArgumentValueResolver" public="false">
36+
<tag name="controller_argument.value_resolver" priority="-100" />
37+
</service>
38+
39+
<service id="argument_value_resolver.variadic_argument_from_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentValueResolver\VariadicArgumentValueResolver" public="false">
40+
<tag name="controller_argument.value_resolver" priority="-150" />
41+
</service>
2142

2243
<service id="response_listener" class="Symfony\Component\HttpKernel\EventListener\ResponseListener">
2344
<tag name="kernel.event_subscriber" />

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ CHANGELOG
44
3.1.0
55
-----
66
* deprecated passing objects as URI attributes to the ESI and SSI renderers
7-
* Added an `ArgumentResolver` with `getArguments()` and the respective interface `ArgumentResolverInterface`
8-
* Deprecated `ControllerResolver::getArguments()`, which uses the `ArgumentResolver` as BC layer by extending it
9-
* The `HttpKernel` now accepts an additional argument for an `ArgumentResolver`
7+
* Added a `LegacyArgumentResolver` with `getArguments()` and the corresponding interface `ArgumentResolverInterface`
8+
* Deprecated `ControllerResolver::getArguments()`, which uses the `LegacyArgumentResolver` as BC layer by extending it
9+
* The `HttpKernel` now accepts an additional argument for an `ArgumentResolverInterface`
10+
* Added the `ArgumentResolver` which features an extension point to resolve arguments in a more dynamic way
1011

1112
3.0.0
1213
-----

src/Symfony/Component/HttpKernel/Controller/ArgumentResolver.php

Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -12,57 +12,64 @@
1212
namespace Symfony\Component\HttpKernel\Controller;
1313

1414
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
1516

1617
/**
17-
* Responsible for the creation of the action arguments.
18+
* Responsible for the resolving of arguments passed to an action.
1819
*
19-
* @author Fabien Potencier <fabien@symfony.com>
20+
* @author Iltar van der Berg <kjarli@gmail.com>
2021
*/
21-
class ArgumentResolver implements ArgumentResolverInterface
22+
final class ArgumentResolver implements ArgumentResolverInterface
2223
{
24+
private $argumentMetadataFactory;
25+
2326
/**
24-
* {@inheritdoc}
27+
* @var ArgumentValueResolverInterface[]
2528
*/
26-
public function getArguments(Request $request, $controller)
27-
{
28-
if (is_array($controller)) {
29-
$r = new \ReflectionMethod($controller[0], $controller[1]);
30-
} elseif (is_object($controller) && !$controller instanceof \Closure) {
31-
$r = new \ReflectionObject($controller);
32-
$r = $r->getMethod('__invoke');
33-
} else {
34-
$r = new \ReflectionFunction($controller);
35-
}
29+
private $argumentValueResolvers;
3630

37-
return $this->doGetArguments($request, $controller, $r->getParameters());
31+
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array())
32+
{
33+
$this->argumentMetadataFactory = $argumentMetadataFactory;
34+
$this->argumentValueResolvers = $argumentValueResolvers;
3835
}
3936

40-
protected function doGetArguments(Request $request, $controller, array $parameters)
37+
/**
38+
* {@inheritdoc}
39+
*/
40+
public function getArguments(Request $request, $controller)
4141
{
42-
$attributes = $request->attributes->all();
4342
$arguments = array();
44-
foreach ($parameters as $param) {
45-
if (array_key_exists($param->name, $attributes)) {
46-
if (PHP_VERSION_ID >= 50600 && $param->isVariadic() && is_array($attributes[$param->name])) {
47-
$arguments = array_merge($arguments, array_values($attributes[$param->name]));
48-
} else {
49-
$arguments[] = $attributes[$param->name];
43+
44+
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) {
45+
foreach ($this->argumentValueResolvers as $resolver) {
46+
if (!$resolver->supports($request, $metadata)) {
47+
continue;
5048
}
51-
} elseif ($param->getClass() && $param->getClass()->isInstance($request)) {
52-
$arguments[] = $request;
53-
} elseif ($param->isDefaultValueAvailable()) {
54-
$arguments[] = $param->getDefaultValue();
55-
} else {
56-
if (is_array($controller)) {
57-
$repr = sprintf('%s::%s()', get_class($controller[0]), $controller[1]);
58-
} elseif (is_object($controller)) {
59-
$repr = get_class($controller);
60-
} else {
61-
$repr = $controller;
49+
50+
$resolved = $resolver->resolve($request, $metadata);
51+
52+
if (!$resolved instanceof \Generator) {
53+
throw new \InvalidArgumentException(sprintf('%s::resolve() must yield at least one value.', get_class($resolver)));
54+
}
55+
56+
foreach ($resolved as $append) {
57+
$arguments[] = $append;
6258
}
6359

64-
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));
60+
// continue to the next controller argument
61+
continue 2;
6562
}
63+
64+
$representative = $controller;
65+
66+
if (is_array($representative)) {
67+
$representative = sprintf('%s::%s()', get_class($representative[0]), $representative[1]);
68+
} elseif (is_object($representative)) {
69+
$representative = get_class($representative);
70+
}
71+
72+
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()));
6673
}
6774

6875
return $arguments;

src/Symfony/Component/HttpKernel/Controller/ArgumentResolverInterface.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ interface ArgumentResolverInterface
2424
/**
2525
* Returns the arguments to pass to the controller.
2626
*
27-
* @param Request $request A Request instance
28-
* @param callable $controller A PHP callable
27+
* @param Request $request
28+
* @param callable $controller
2929
*
3030
* @return array An array of arguments to pass to the controller
3131
*
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
18+
/**
19+
* Grabs a non-variadic value from the request and returns it.
20+
*
21+
* @author Iltar van der Berg <kjarli@gmail.com>
22+
*/
23+
final class ArgumentFromAttributeResolver implements ArgumentValueResolverInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function supports(Request $request, ArgumentMetadata $argument)
29+
{
30+
return !$argument->isVariadic() && $request->attributes->has($argument->getName());
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function resolve(Request $request, ArgumentMetadata $argument)
37+
{
38+
yield $request->attributes->get($argument->getName());
39+
}
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
18+
/**
19+
* Returns the default value defined in the action signature if present and no value has been given.
20+
*
21+
* @author Iltar van der Berg <kjarli@gmail.com>
22+
*/
23+
final class DefaultArgumentValueResolver implements ArgumentValueResolverInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function supports(Request $request, ArgumentMetadata $argument)
29+
{
30+
return $argument->hasDefaultValue() && !$request->attributes->has($argument->getName());
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function resolve(Request $request, ArgumentMetadata $argument)
37+
{
38+
yield $argument->getDefaultValue();
39+
}
40+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\HttpKernel\Controller\ArgumentValueResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
18+
/**
19+
* Supports the same instance as the request object passed along.
20+
*
21+
* @author Iltar van der Berg <kjarli@gmail.com>
22+
*/
23+
final class RequestResolver implements ArgumentValueResolverInterface
24+
{
25+
/**
26+
* {@inheritdoc}
27+
*/
28+
public function supports(Request $request, ArgumentMetadata $argument)
29+
{
30+
return $argument->getType() === Request::class || is_subclass_of($request, $argument->getType());
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function resolve(Request $request, ArgumentMetadata $argument)
37+
{
38+
yield $request;
39+
}
40+
}

0 commit comments

Comments
 (0)
0