Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit f0e076a

Browse filesBrowse files
committed
feature #40307 [HttpKernel] Handle multi-attribute controller arguments (chalasr)
This PR was merged into the 5.3-dev branch. Discussion ---------- [HttpKernel] Handle multi-attribute controller arguments | Q | A | ------------- | --- | Branch? | 5.x | Bug fix? | no | New feature? | yes | Deprecations? | yes | Tickets | - | License | MIT | Doc PR | todo Currently, the `ArgumentMetadata` class used for controller argument value resolution can only hold one attribute per controller argument, while a method argument can take multiple attributes. This allows accessing all attributes for a given argument, and deprecates the `ArgumentInterface` because it is not needed. Spotted by @nicolas-grekas. Commits ------- d771e44 [HttpKernel] Handle multi-attribute controller arguments
2 parents 4a9c829 + d771e44 commit f0e076a
Copy full SHA for f0e076a

File tree

14 files changed

+103
-39
lines changed
Filter options

14 files changed

+103
-39
lines changed

‎UPGRADE-5.3.md

Copy file name to clipboardExpand all lines: UPGRADE-5.3.md
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ HttpFoundation
4444
HttpKernel
4545
----------
4646

47+
* Deprecate `ArgumentInterface`
48+
* Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead
4749
* Marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
4850

4951
Messenger

‎UPGRADE-6.0.md

Copy file name to clipboardExpand all lines: UPGRADE-6.0.md
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ HttpFoundation
9292
HttpKernel
9393
----------
9494

95+
* Remove `ArgumentInterface`
96+
* Remove `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead
9597
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
9698
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
9799

‎src/Symfony/Component/HttpKernel/Attribute/ArgumentInterface.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Attribute/ArgumentInterface.php
+4Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111

1212
namespace Symfony\Component\HttpKernel\Attribute;
1313

14+
trigger_deprecation('symfony/http-kernel', '5.3', 'The "%s" interface is deprecated.', ArgumentInterface::class);
15+
1416
/**
1517
* Marker interface for controller argument attributes.
18+
*
19+
* @deprecated since Symfony 5.3
1620
*/
1721
interface ArgumentInterface
1822
{

‎src/Symfony/Component/HttpKernel/CHANGELOG.md

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/CHANGELOG.md
+3Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ CHANGELOG
44
5.3
55
---
66

7+
* Deprecate `ArgumentInterface`
8+
* Add `ArgumentMetadata::getAttributes()`
9+
* Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead
710
* marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
811

912
5.2.0

‎src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadata.php
+48-4Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,34 @@
2020
*/
2121
class ArgumentMetadata
2222
{
23+
public const IS_INSTANCEOF = 2;
24+
2325
private $name;
2426
private $type;
2527
private $isVariadic;
2628
private $hasDefaultValue;
2729
private $defaultValue;
2830
private $isNullable;
29-
private $attribute;
31+
private $attributes;
3032

31-
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, ?ArgumentInterface $attribute = null)
33+
/**
34+
* @param object[] $attributes
35+
*/
36+
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, $attributes = [])
3237
{
3338
$this->name = $name;
3439
$this->type = $type;
3540
$this->isVariadic = $isVariadic;
3641
$this->hasDefaultValue = $hasDefaultValue;
3742
$this->defaultValue = $defaultValue;
3843
$this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue);
39-
$this->attribute = $attribute;
44+
45+
if (null === $attributes || $attributes instanceof ArgumentInterface) {
46+
trigger_deprecation('symfony/http-kernel', '5.3', 'The "%s" constructor expects an array of PHP attributes as last argument, %s given.', __CLASS__, get_debug_type($attributes));
47+
$attributes = $attributes ? [$attributes] : [];
48+
}
49+
50+
$this->attributes = $attributes;
4051
}
4152

4253
/**
@@ -114,6 +125,39 @@ public function getDefaultValue()
114125
*/
115126
public function getAttribute(): ?ArgumentInterface
116127
{
117-
return $this->attribute;
128+
trigger_deprecation('symfony/http-kernel', '5.3', 'Method "%s()" is deprecated, use "getAttributes()" instead.', __METHOD__);
129+
130+
if (!$this->attributes) {
131+
return null;
132+
}
133+
134+
return $this->attributes[0] instanceof ArgumentInterface ? $this->attributes[0] : null;
135+
}
136+
137+
/**
138+
* @return object[]
139+
*/
140+
public function getAttributes(string $name = null, int $flags = 0): array
141+
{
142+
if (!$name) {
143+
return $this->attributes;
144+
}
145+
146+
$attributes = [];
147+
if ($flags & self::IS_INSTANCEOF) {
148+
foreach ($this->attributes as $attribute) {
149+
if ($attribute instanceof $name) {
150+
$attributes[] = $attribute;
151+
}
152+
}
153+
} else {
154+
foreach ($this->attributes as $attribute) {
155+
if (\get_class($attribute) === $name) {
156+
$attributes[] = $attribute;
157+
}
158+
}
159+
}
160+
161+
return $attributes;
118162
}
119163
}

‎src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/ControllerMetadata/ArgumentMetadataFactory.php
+4-20Lines changed: 4 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,6 @@
1111

1212
namespace Symfony\Component\HttpKernel\ControllerMetadata;
1313

14-
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
15-
use Symfony\Component\HttpKernel\Exception\InvalidMetadataException;
16-
1714
/**
1815
* Builds {@see ArgumentMetadata} objects based on the given Controller.
1916
*
@@ -37,28 +34,15 @@ public function createArgumentMetadata($controller): array
3734
}
3835

3936
foreach ($reflection->getParameters() as $param) {
40-
$attribute = null;
4137
if (\PHP_VERSION_ID >= 80000) {
42-
$reflectionAttributes = $param->getAttributes(ArgumentInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
43-
44-
if (\count($reflectionAttributes) > 1) {
45-
$representative = $controller;
46-
47-
if (\is_array($representative)) {
48-
$representative = sprintf('%s::%s()', \get_class($representative[0]), $representative[1]);
49-
} elseif (\is_object($representative)) {
50-
$representative = \get_class($representative);
38+
foreach ($param->getAttributes() as $reflectionAttribute) {
39+
if (class_exists($reflectionAttribute->getName())) {
40+
$attributes[] = $reflectionAttribute->newInstance();
5141
}
52-
53-
throw new InvalidMetadataException(sprintf('Controller "%s" has more than one attribute for "$%s" argument.', $representative, $param->getName()));
54-
}
55-
56-
if (isset($reflectionAttributes[0])) {
57-
$attribute = $reflectionAttributes[0]->newInstance();
5842
}
5943
}
6044

61-
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attribute);
45+
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes ?? []);
6246
}
6347

6448
return $arguments;

‎src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php
+4-6Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
use PHPUnit\Framework\TestCase;
1616
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
1717
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
18-
use Symfony\Component\HttpKernel\Exception\InvalidMetadataException;
1918
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
2019
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
2120
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
@@ -128,18 +127,17 @@ public function testAttributeSignature()
128127
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
129128

130129
$this->assertEquals([
131-
new ArgumentMetadata('baz', 'string', false, false, null, false, new Foo('bar')),
130+
new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]),
132131
], $arguments);
133132
}
134133

135134
/**
136135
* @requires PHP 8
137136
*/
138-
public function testAttributeSignatureError()
137+
public function testMultipleAttributes()
139138
{
140-
$this->expectException(InvalidMetadataException::class);
141-
142-
$this->factory->createArgumentMetadata([new AttributeController(), 'invalidAction']);
139+
$this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg']);
140+
$this->assertCount(1, $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg'])[0]->getAttributes());
143141
}
144142

145143
private function signature1(self $foo, array $bar, callable $baz)

‎src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/ControllerMetadata/ArgumentMetadataTest.php
+28Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,15 @@
1212
namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata;
1313

1414
use PHPUnit\Framework\TestCase;
15+
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
16+
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
1517
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
1619

1720
class ArgumentMetadataTest extends TestCase
1821
{
22+
use ExpectDeprecationTrait;
23+
1924
public function testWithBcLayerWithDefault()
2025
{
2126
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value');
@@ -41,4 +46,27 @@ public function testDefaultValueUnavailable()
4146
$this->assertFalse($argument->hasDefaultValue());
4247
$argument->getDefaultValue();
4348
}
49+
50+
/**
51+
* @group legacy
52+
*/
53+
public function testLegacyAttribute()
54+
{
55+
$attribute = $this->createMock(ArgumentInterface::class);
56+
57+
$this->expectDeprecation('Since symfony/http-kernel 5.3: The "Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata" constructor expects an array of PHP attributes as last argument, %s given.');
58+
$this->expectDeprecation('Since symfony/http-kernel 5.3: Method "Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata::getAttribute()" is deprecated, use "getAttributes()" instead.');
59+
60+
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, $attribute);
61+
$this->assertSame($attribute, $argument->getAttribute());
62+
}
63+
64+
/**
65+
* @requires PHP 8
66+
*/
67+
public function testGetAttributes()
68+
{
69+
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new Foo('bar')]);
70+
$this->assertEquals([new Foo('bar')], $argument->getAttributes());
71+
}
4472
}

‎src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Foo.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/Fixtures/Attribute/Foo.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
1515

1616
#[\Attribute(\Attribute::TARGET_PARAMETER)]
17-
class Foo implements ArgumentInterface
17+
class Foo
1818
{
1919
private $foo;
2020

‎src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/HttpKernel/Tests/Fixtures/Controller/AttributeController.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ class AttributeController
1818
public function action(#[Foo('bar')] string $baz) {
1919
}
2020

21-
public function invalidAction(#[Foo('bar'), Foo('bar')] string $baz) {
21+
public function multiAttributeArg(#[Foo('bar'), Undefined('bar')] string $baz) {
2222
}
2323
}

‎src/Symfony/Component/Security/Http/Attribute/CurrentUser.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Attribute/CurrentUser.php
+1-3Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111

1212
namespace Symfony\Component\Security\Http\Attribute;
1313

14-
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
15-
1614
/**
1715
* Indicates that a controller argument should receive the current logged user.
1816
*/
1917
#[\Attribute(\Attribute::TARGET_PARAMETER)]
20-
class CurrentUser implements ArgumentInterface
18+
class CurrentUser
2119
{
2220
}

‎src/Symfony/Component/Security/Http/Controller/UserValueResolver.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Controller/UserValueResolver.php
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public function supports(Request $request, ArgumentMetadata $argument): bool
3737
{
3838
// with the attribute, the type can be any UserInterface implementation
3939
// otherwise, the type must be UserInterface
40-
if (UserInterface::class !== $argument->getType() && !$argument->getAttribute() instanceof CurrentUser) {
40+
if (UserInterface::class !== $argument->getType() && !$argument->getAttributes(CurrentUser::class, ArgumentMetadata::IS_INSTANCEOF)) {
4141
return false;
4242
}
4343

‎src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/Tests/Controller/UserValueResolverTest.php
+3-2Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@ public function testResolveWithAttribute()
7777
$tokenStorage->setToken($token);
7878

7979
$resolver = new UserValueResolver($tokenStorage);
80-
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, new CurrentUser());
80+
$metadata = $this->createMock(ArgumentMetadata::class);
81+
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, [new CurrentUser()]);
8182

8283
$this->assertTrue($resolver->supports(Request::create('/'), $metadata));
8384
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
@@ -89,7 +90,7 @@ public function testResolveWithAttributeAndNoUser()
8990
$tokenStorage->setToken(new UsernamePasswordToken('username', 'password', 'provider'));
9091

9192
$resolver = new UserValueResolver($tokenStorage);
92-
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, new CurrentUser());
93+
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, [new CurrentUser()]);
9394

9495
$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
9596
}

‎src/Symfony/Component/Security/Http/composer.json

Copy file name to clipboardExpand all lines: src/Symfony/Component/Security/Http/composer.json
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
"symfony/deprecation-contracts": "^2.1",
2121
"symfony/security-core": "^5.3",
2222
"symfony/http-foundation": "^5.2",
23-
"symfony/http-kernel": "^5.2",
23+
"symfony/http-kernel": "^5.3",
2424
"symfony/polyfill-php80": "^1.15",
2525
"symfony/property-access": "^4.4|^5.0"
2626
},

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.