Skip to content

Commit f7f6e36

Browse files
jvasseurfabpot
authored andcommitted
[RFC][HttpKernel][Security] Allowed adding attributes on controller arguments that will be passed to argument resolvers.
1 parent 41ab040 commit f7f6e36

File tree

8 files changed

+148
-2
lines changed

8 files changed

+148
-2
lines changed

Attribute/ArgumentInterface.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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\Attribute;
13+
14+
/**
15+
* Marker interface for controller argument attributes.
16+
*/
17+
interface ArgumentInterface
18+
{
19+
}

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ CHANGELOG
1010
`kernel.trusted_proxies` and `kernel.trusted_headers` parameters
1111
* content of request parameter `_password` is now also hidden
1212
in the request profiler raw content section
13+
* Allowed adding attributes on controller arguments that will be passed to argument resolvers.
1314

1415
5.1.0
1516
-----

ControllerMetadata/ArgumentMetadata.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\HttpKernel\ControllerMetadata;
1313

14+
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
15+
1416
/**
1517
* Responsible for storing metadata of an argument.
1618
*
@@ -24,15 +26,17 @@ class ArgumentMetadata
2426
private $hasDefaultValue;
2527
private $defaultValue;
2628
private $isNullable;
29+
private $attribute;
2730

28-
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false)
31+
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, ?ArgumentInterface $attribute = null)
2932
{
3033
$this->name = $name;
3134
$this->type = $type;
3235
$this->isVariadic = $isVariadic;
3336
$this->hasDefaultValue = $hasDefaultValue;
3437
$this->defaultValue = $defaultValue;
3538
$this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue);
39+
$this->attribute = $attribute;
3640
}
3741

3842
/**
@@ -104,4 +108,12 @@ public function getDefaultValue()
104108

105109
return $this->defaultValue;
106110
}
111+
112+
/**
113+
* Returns the attribute (if any) that was set on the argument.
114+
*/
115+
public function getAttribute(): ?ArgumentInterface
116+
{
117+
return $this->attribute;
118+
}
107119
}

ControllerMetadata/ArgumentMetadataFactory.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Symfony\Component\HttpKernel\ControllerMetadata;
1313

14+
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
15+
use Symfony\Component\HttpKernel\Exception\InvalidMetadataException;
16+
1417
/**
1518
* Builds {@see ArgumentMetadata} objects based on the given Controller.
1619
*
@@ -34,7 +37,28 @@ public function createArgumentMetadata($controller): array
3437
}
3538

3639
foreach ($reflection->getParameters() as $param) {
37-
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull());
40+
$attribute = null;
41+
if (method_exists($param, 'getAttributes')) {
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+
}
59+
}
60+
61+
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attribute);
3862
}
3963

4064
return $arguments;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
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\Exception;
13+
14+
class InvalidMetadataException extends \LogicException
15+
{
16+
}

Tests/ControllerMetadata/ArgumentMetadataFactoryTest.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
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;
20+
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
1821
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
1922
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
2023
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController;
@@ -117,6 +120,28 @@ public function testNullableTypesSignature()
117120
], $arguments);
118121
}
119122

123+
/**
124+
* @requires PHP 8
125+
*/
126+
public function testAttributeSignature()
127+
{
128+
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
129+
130+
$this->assertEquals([
131+
new ArgumentMetadata('baz', 'string', false, false, null, false, new Foo('bar')),
132+
], $arguments);
133+
}
134+
135+
/**
136+
* @requires PHP 8
137+
*/
138+
public function testAttributeSignatureError()
139+
{
140+
$this->expectException(InvalidMetadataException::class);
141+
142+
$this->factory->createArgumentMetadata([new AttributeController(), 'invalidAction']);
143+
}
144+
120145
private function signature1(self $foo, array $bar, callable $baz)
121146
{
122147
}

Tests/Fixtures/Attribute/Foo.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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\Tests\Fixtures\Attribute;
13+
14+
use Attribute;
15+
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
16+
17+
#[Attribute(Attribute::TARGET_PARAMETER)]
18+
class Foo implements ArgumentInterface
19+
{
20+
private $foo;
21+
22+
public function __construct($foo)
23+
{
24+
$this->foo = $foo;
25+
}
26+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
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\Tests\Fixtures\Controller;
13+
14+
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
15+
16+
class AttributeController
17+
{
18+
public function action(#[Foo('bar')] string $baz) {
19+
}
20+
21+
public function invalidAction(#[Foo('bar'), Foo('bar')] string $baz) {
22+
}
23+
}

0 commit comments

Comments
 (0)