Skip to content

Commit 2881f50

Browse files
author
Iltar van der Berg
committed
Added an ArgumentResolver with clean extension point
1 parent 7adba7f commit 2881f50

25 files changed

+989
-102
lines changed

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

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;

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+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 the variadic value from the request and returns it.
20+
*
21+
* @author Iltar van der Berg <kjarli@gmail.com>
22+
*/
23+
final class VariadicArgumentValueResolver 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+
$values = $request->attributes->get($argument->getName());
39+
40+
if (!is_array($values)) {
41+
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)));
42+
}
43+
44+
foreach ($values as $value) {
45+
yield $value;
46+
}
47+
}
48+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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\ControllerMetadata\ArgumentMetadata;
16+
17+
/**
18+
* Responsible for resolving the value of an argument based on its metadata.
19+
*
20+
* @author Iltar van der Berg <kjarli@gmail.com>
21+
*/
22+
interface ArgumentValueResolverInterface
23+
{
24+
/**
25+
* Whether this resolver can resolve can resolve the value for the given ArgumentMetadata.
26+
*
27+
* @param Request $request
28+
* @param ArgumentMetadata $argument
29+
*
30+
* @return bool
31+
*/
32+
public function supports(Request $request, ArgumentMetadata $argument);
33+
34+
/**
35+
* Yield the possible value(s).
36+
*
37+
* An implementation must yield at least one value.
38+
*
39+
* @param Request $request
40+
* @param ArgumentMetadata $argument
41+
*
42+
* @return \Generator
43+
*/
44+
public function resolve(Request $request, ArgumentMetadata $argument);
45+
}

Controller/ControllerResolver.php

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*
2424
* @author Fabien Potencier <fabien@symfony.com>
2525
*/
26-
class ControllerResolver extends ArgumentResolver implements ControllerResolverInterface
26+
class ControllerResolver extends LegacyArgumentResolver implements ControllerResolverInterface
2727
{
2828
private $logger;
2929

@@ -85,21 +85,21 @@ public function getController(Request $request)
8585
/**
8686
* {@inheritdoc}
8787
*
88-
* @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the ArgumentResolver instead.
88+
* @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead.
8989
*/
9090
public function getArguments(Request $request, $controller)
9191
{
92-
@trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED);
92+
@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);
9393

9494
return parent::getArguments($request, $controller);
9595
}
9696

9797
/**
98-
* @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the ArgumentResolver instead.
98+
* @deprecated this method is deprecated as of 3.1 and will be removed in 4.0. Implement the ArgumentResolverInterface or extend the LegacyArgumentResolver instead.
9999
*/
100100
protected function doGetArguments(Request $request, $controller, array $parameters)
101101
{
102-
@trigger_error(sprintf('%s is deprecated as of 3.1 and will be removed in 4.0. Implement the %s or extend the %s and inject it in the HttpKernel instead.', __METHOD__, ArgumentResolverInterface::class, ArgumentResolver::class), E_USER_DEPRECATED);
102+
@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);
103103

104104
return parent::doGetArguments($request, $controller, $parameters);
105105
}

0 commit comments

Comments
 (0)