Skip to content

Commit e55c7a8

Browse files
committed
[HttpKernel] Handle multi-attribute controller arguments
1 parent adbb341 commit e55c7a8

File tree

11 files changed

+88
-36
lines changed

11 files changed

+88
-36
lines changed

UPGRADE-5.3.md

Lines 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()` and `hasAttributes()` instead
4749
* Marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
4850

4951
Messenger

UPGRADE-6.0.md

Lines 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()` and `hasAttribute()` 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

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
{

src/Symfony/Component/HttpKernel/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ CHANGELOG
44
5.3
55
---
66

7+
* Deprecate `ArgumentInterface`
8+
* Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` and `hasAttribute()` instead
79
* marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
810

911
5.2.0

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

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,26 @@ class ArgumentMetadata
2626
private $hasDefaultValue;
2727
private $defaultValue;
2828
private $isNullable;
29-
private $attribute;
29+
private $attributes;
3030

31-
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, ?ArgumentInterface $attribute = null)
31+
/**
32+
* @param \ReflectionAttribute[] $attributes
33+
*/
34+
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, $attributes = [])
3235
{
3336
$this->name = $name;
3437
$this->type = $type;
3538
$this->isVariadic = $isVariadic;
3639
$this->hasDefaultValue = $hasDefaultValue;
3740
$this->defaultValue = $defaultValue;
3841
$this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue);
39-
$this->attribute = $attribute;
42+
43+
if (null === $attributes || $attributes instanceof ArgumentInterface) {
44+
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));
45+
$attributes = $attributes ? [$attributes] : [];
46+
}
47+
48+
$this->attributes = $attributes;
4049
}
4150

4251
/**
@@ -114,6 +123,31 @@ public function getDefaultValue()
114123
*/
115124
public function getAttribute(): ?ArgumentInterface
116125
{
117-
return $this->attribute;
126+
trigger_deprecation('symfony/http-kernel', '5.3', 'Method "%s()" is deprecated, use "getAttributes()" instead.', __METHOD__);
127+
128+
if (!$this->attributes) {
129+
return null;
130+
}
131+
132+
return $this->attributes[0] instanceof ArgumentInterface ? $this->attributes[0] : null;
133+
}
134+
135+
public function hasAttribute(string $type): bool
136+
{
137+
foreach ($this->attributes as $attribute) {
138+
if ($attribute->getName() === $type) {
139+
return true;
140+
}
141+
}
142+
143+
return false;
144+
}
145+
146+
/**
147+
* @return \ReflectionAttribute[]
148+
*/
149+
public function getAttributes(): array
150+
{
151+
return $this->attributes;
118152
}
119153
}

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

Lines changed: 2 additions & 22 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,11 @@ 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);
51-
}
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();
58-
}
38+
$attributes = $param->getAttributes();
5939
}
6040

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

6444
return $arguments;

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

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +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;
19-
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
2018
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
2119
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
2220
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
@@ -128,18 +126,17 @@ public function testAttributeSignature()
128126
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
129127

130128
$this->assertEquals([
131-
new ArgumentMetadata('baz', 'string', false, false, null, false, new Foo('bar')),
129+
new ArgumentMetadata('baz', 'string', false, false, null, false, (new \ReflectionParameter([AttributeController::class, 'action'], 'baz'))->getAttributes()),
132130
], $arguments);
133131
}
134132

135133
/**
136134
* @requires PHP 8
137135
*/
138-
public function testAttributeSignatureError()
136+
public function testMultipleAttributes()
139137
{
140-
$this->expectException(InvalidMetadataException::class);
141-
142-
$this->factory->createArgumentMetadata([new AttributeController(), 'invalidAction']);
138+
$this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg']);
139+
$this->assertCount(2, $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg'])[0]->getAttributes());
143140
}
144141

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

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,14 @@
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;
1618

1719
class ArgumentMetadataTest extends TestCase
1820
{
21+
use ExpectDeprecationTrait;
22+
1923
public function testWithBcLayerWithDefault()
2024
{
2125
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value');
@@ -41,4 +45,31 @@ public function testDefaultValueUnavailable()
4145
$this->assertFalse($argument->hasDefaultValue());
4246
$argument->getDefaultValue();
4347
}
48+
49+
/**
50+
* @group legacy
51+
*/
52+
public function testLegacyAttribute()
53+
{
54+
$attribute = $this->createMock(ArgumentInterface::class);
55+
56+
$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.');
57+
$this->expectDeprecation('Since symfony/http-kernel 5.3: Method "Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata::getAttribute()" is deprecated, use "getAttributes()" instead.');
58+
59+
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, $attribute);
60+
$this->assertSame($attribute, $argument->getAttribute());
61+
}
62+
63+
/**
64+
* @requires PHP 8
65+
*/
66+
public function testGetAttributes()
67+
{
68+
$controller = function (#[Baz] string $foo = 'default value') {};
69+
70+
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, (new \ReflectionFunction($controller))->getParameters()[0]->getAttributes());
71+
$this->assertTrue($argument->hasAttribute(Baz::class));
72+
$this->assertCount(1, $argument->getAttributes());
73+
$this->assertInstanceOf(\ReflectionAttribute::class, $argument->getAttributes()[0]);
74+
}
4475
}

src/Symfony/Component/HttpKernel/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

src/Symfony/Component/HttpKernel/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'), Foo('bar')] string $baz) {
2222
}
2323
}

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