Skip to content

Commit d162d45

Browse files
committed
[HttpKernel] Handle multi-attribute controller arguments
1 parent a705a43 commit d162d45

File tree

8 files changed

+93
-32
lines changed

8 files changed

+93
-32
lines changed

Attribute/ArgumentInterface.php

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

CHANGELOG.md

Lines 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

ControllerMetadata/ArgumentMetadata.php

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

ControllerMetadata/ArgumentMetadataFactory.php

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

Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php

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

Tests/ControllerMetadata/ArgumentMetadataTest.php

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

Tests/Fixtures/Attribute/Foo.php

Lines 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

Tests/Fixtures/Controller/AttributeController.php

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

0 commit comments

Comments
 (0)