Skip to content

Commit cef7e5b

Browse files
committed
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](4c092b3) 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
2 parents 162338e + 1bf80c9 commit cef7e5b

39 files changed

+1486
-98
lines changed

UPGRADE-3.1.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,11 @@ HttpKernel
6666
deprecated and will be removed in Symfony 4.0. The inline fragment
6767
renderer should be used with object attributes.
6868

69+
* The `ControllerResolver::getArguments()` method has been deprecated and will
70+
be removed in 4.0. If you have your own `ControllerResolverInterface`
71+
implementation, you should inject either an `ArgumentResolverInterface`
72+
instance or the new `ArgumentResolver` in the `HttpKernel`.
73+
6974
Serializer
7075
----------
7176

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: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,6 @@ public function load(array $configs, ContainerBuilder $container)
150150

151151
$loader->load('debug.xml');
152152

153-
$definition = $container->findDefinition('http_kernel');
154-
$definition->replaceArgument(1, new Reference('debug.controller_resolver'));
155-
156153
// replace the regular event_dispatcher service with the debug one
157154
$definition = $container->findDefinition('event_dispatcher');
158155
$definition->setPublic(false);
@@ -173,6 +170,9 @@ public function load(array $configs, ContainerBuilder $container)
173170
'Symfony\\Component\\HttpKernel\\EventListener\\ResponseListener',
174171
'Symfony\\Component\\HttpKernel\\EventListener\\RouterListener',
175172
'Symfony\\Component\\HttpKernel\\Controller\\ControllerResolver',
173+
'Symfony\\Component\\HttpKernel\\Controller\\ArgumentResolver',
174+
'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadata',
175+
'Symfony\\Component\\HttpKernel\\ControllerMetadata\\ArgumentMetadataFactory',
176176
'Symfony\\Component\\HttpKernel\\Event\\KernelEvent',
177177
'Symfony\\Component\\HttpKernel\\Event\\FilterControllerEvent',
178178
'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/debug.xml

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,14 @@
1717
<argument type="service" id="logger" on-invalid="null" />
1818
</service>
1919

20-
<service id="debug.controller_resolver" class="Symfony\Component\HttpKernel\Controller\TraceableControllerResolver">
21-
<argument type="service" id="controller_resolver" />
20+
<service id="debug.controller_resolver" decorates="controller_resolver" class="Symfony\Component\HttpKernel\Controller\TraceableControllerResolver">
21+
<argument type="service" id="debug.controller_resolver.inner" />
22+
<argument type="service" id="debug.stopwatch" />
23+
<argument type="service" id="argument_resolver" />
24+
</service>
25+
26+
<service id="debug.argument_resolver" decorates="argument_resolver" class="Symfony\Component\HttpKernel\Controller\TraceableArgumentResolver">
27+
<argument type="service" id="debug.argument_resolver.inner" />
2228
<argument type="service" id="debug.stopwatch" />
2329
</service>
2430
</services>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
<argument type="service" id="event_dispatcher" />
1414
<argument type="service" id="controller_resolver" />
1515
<argument type="service" id="request_stack" />
16+
<argument type="service" id="argument_resolver" />
1617
</service>
1718

1819
<service id="request_stack" class="Symfony\Component\HttpFoundation\RequestStack" />

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,29 @@
1717
<argument type="service" id="logger" on-invalid="ignore" />
1818
</service>
1919

20+
<service id="argument_metadata_factory" class="Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory" public="false" />
21+
22+
<service id="argument_resolver" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver" public="false">
23+
<argument type="service" id="argument_metadata_factory" />
24+
<argument type="collection" />
25+
</service>
26+
27+
<service id="argument_resolver.request_attribute" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver" public="false">
28+
<tag name="controller_argument.value_resolver" priority="100" />
29+
</service>
30+
31+
<service id="argument_resolver.request" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver" public="false">
32+
<tag name="controller_argument.value_resolver" priority="50" />
33+
</service>
34+
35+
<service id="argument_resolver.default" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver" public="false">
36+
<tag name="controller_argument.value_resolver" priority="-100" />
37+
</service>
38+
39+
<service id="argument_resolver.variadic" class="Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver" public="false">
40+
<tag name="controller_argument.value_resolver" priority="-150" />
41+
</service>
42+
2043
<service id="response_listener" class="Symfony\Component\HttpKernel\EventListener\ResponseListener">
2144
<tag name="kernel.event_subscriber" />
2245
<argument>%kernel.charset%</argument>

src/Symfony/Bundle/FrameworkBundle/composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
"symfony/config": "~2.8|~3.0",
2424
"symfony/event-dispatcher": "~2.8|~3.0",
2525
"symfony/http-foundation": "~3.1",
26-
"symfony/http-kernel": "~2.8|~3.0",
26+
"symfony/http-kernel": "~3.1",
2727
"symfony/polyfill-mbstring": "~1.0",
2828
"symfony/filesystem": "~2.8|~3.0",
2929
"symfony/finder": "~2.8|~3.0",

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@ CHANGELOG
44
3.1.0
55
-----
66
* deprecated passing objects as URI attributes to the ESI and SSI renderers
7+
* added `Symfony\Component\HttpKernel\Controller\LegacyArgumentResolver`
8+
* deprecated `ControllerResolver::getArguments()`
9+
* made `ControllerResolver` extend the `LegacyArgumentResolver` for BC
10+
* added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface`
11+
* added `Symfony\Component\HttpKernel\Controller\ArgumentResolverInterface` as argument to `HttpKernel`
12+
* added `Symfony\Component\HttpKernel\Controller\ArgumentResolver`
713

814
3.0.0
915
-----
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
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;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestAttributeValueResolver;
17+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\RequestValueResolver;
18+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\VariadicValueResolver;
19+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
20+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactoryInterface;
21+
22+
/**
23+
* Responsible for resolving the arguments passed to an action.
24+
*
25+
* @author Iltar van der Berg <kjarli@gmail.com>
26+
*/
27+
final class ArgumentResolver implements ArgumentResolverInterface
28+
{
29+
private $argumentMetadataFactory;
30+
31+
/**
32+
* @var ArgumentValueResolverInterface[]
33+
*/
34+
private $argumentValueResolvers;
35+
36+
public function __construct(ArgumentMetadataFactoryInterface $argumentMetadataFactory = null, array $argumentValueResolvers = array())
37+
{
38+
$this->argumentMetadataFactory = $argumentMetadataFactory ?: new ArgumentMetadataFactory();
39+
$this->argumentValueResolvers = $argumentValueResolvers ?: array(
40+
new RequestAttributeValueResolver(),
41+
new RequestValueResolver(),
42+
new DefaultValueResolver(),
43+
new VariadicValueResolver(),
44+
);
45+
}
46+
47+
/**
48+
* {@inheritdoc}
49+
*/
50+
public function getArguments(Request $request, $controller)
51+
{
52+
$arguments = array();
53+
54+
foreach ($this->argumentMetadataFactory->createArgumentMetadata($controller) as $metadata) {
55+
foreach ($this->argumentValueResolvers as $resolver) {
56+
if (!$resolver->supports($request, $metadata)) {
57+
continue;
58+
}
59+
60+
$resolved = $resolver->resolve($request, $metadata);
61+
62+
if (!$resolved instanceof \Generator) {
63+
throw new \InvalidArgumentException(sprintf('%s::resolve() must yield at least one value.', get_class($resolver)));
64+
}
65+
66+
foreach ($resolved as $append) {
67+
$arguments[] = $append;
68+
}
69+
70+
// continue to the next controller argument
71+
continue 2;
72+
}
73+
74+
$representative = $controller;
75+
76+
if (is_array($representative)) {
77+
$representative = sprintf('%s::%s()', get_class($representative[0]), $representative[1]);
78+
} elseif (is_object($representative)) {
79+
$representative = get_class($representative);
80+
}
81+
82+
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()));
83+
}
84+
85+
return $arguments;
86+
}
87+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

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

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


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy