Skip to content

Commit cf03bcb

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

File tree

10 files changed

+81
-13
lines changed

10 files changed

+81
-13
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: 35 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 object[] $attributes A list of PHP Attribute instances
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,28 @@ 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 instanceof $type) {
139+
return true;
140+
}
141+
}
142+
143+
return false;
144+
}
145+
146+
public function getAttributes(): array
147+
{
148+
return $this->attributes;
118149
}
119150
}

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

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\HttpKernel\ControllerMetadata;
1313

14-
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
1514
use Symfony\Component\HttpKernel\Exception\InvalidMetadataException;
1615

1716
/**
@@ -37,9 +36,9 @@ public function createArgumentMetadata($controller): array
3736
}
3837

3938
foreach ($reflection->getParameters() as $param) {
40-
$attribute = null;
39+
$attributes = [];
4140
if (\PHP_VERSION_ID >= 80000) {
42-
$reflectionAttributes = $param->getAttributes(ArgumentInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
41+
$reflectionAttributes = $param->getAttributes();
4342

4443
if (\count($reflectionAttributes) > 1) {
4544
$representative = $controller;
@@ -53,12 +52,12 @@ public function createArgumentMetadata($controller): array
5352
throw new InvalidMetadataException(sprintf('Controller "%s" has more than one attribute for "$%s" argument.', $representative, $param->getName()));
5453
}
5554

56-
if (isset($reflectionAttributes[0])) {
57-
$attribute = $reflectionAttributes[0]->newInstance();
55+
foreach ($reflectionAttributes as $reflectionAttribute) {
56+
$attributes[] = $reflectionAttribute->newInstance();
5857
}
5958
}
6059

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

6463
return $arguments;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ public function testAttributeSignature()
128128
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
129129

130130
$this->assertEquals([
131-
new ArgumentMetadata('baz', 'string', false, false, null, false, new Foo('bar')),
131+
new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]),
132132
], $arguments);
133133
}
134134

src/Symfony/Component/HttpKernel/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+
public function testGetAttributes()
65+
{
66+
$attribute = new Foo('bar');
67+
68+
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [$attribute]);
69+
$this->assertTrue($argument->hasAttribute(Foo::class));
70+
$this->assertSame([$attribute], $argument->getAttributes());
71+
}
4472
}

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/Security/Http/Controller/UserValueResolver.php

Lines 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->hasAttribute(CurrentUser::class)) {
4141
return false;
4242
}
4343

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