8000 [RFC][HttpKernel][Security] Allowed adding attributes on controller a… · symfony/symfony@20f3169 · GitHub
[go: up one dir, main page]

Skip to content

Commit 20f3169

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

File tree

13 files changed

+193
-3
lines changed

13 files changed

+193
-3
lines changed
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+
}

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

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

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

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

src/Symfony/Component/Security/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ CHANGELOG
1111
* Deprecated `setProviderKey()`/`getProviderKey()` in favor of `setFirewallName()/getFirewallName()` in `PreAuthenticatedToken`, `RememberMeToken`, `SwitchUserToken`, `UsernamePasswordToken`, `DefaultAuthenticationSuccessHandler`; and deprecated the `AbstractRememberMeServices::$providerKey` property in favor of `AbstractRememberMeServices::$firewallName`
1212
* Added `FirewallListenerInterface` to make the execution order of firewall listeners configurable
1313
* Added translator to `\Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator` and `\Symfony\Component\Security\Http\Firewall\UsernamePasswordJsonAuthenticationListener` to translate authentication failure messages
14+
* Added a CurrentUser attribute to force the UserValueResolver to resolve an argument to the current user.
1415

1516
5.1.0
1617
-----
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\Security\Http\Attribute;
13+
14+
use Attribute;
15+
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
16+
17+
/**
18+
* Indicates that a controller argument should receive the current logged user.
19+
*/
20+
#[Attribute(Attribute::TARGET_PARAMETER)]
21+
class CurrentUser implements ArgumentInterface
22+
{
23+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1818
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1919
use Symfony\Component\Security\Core\User\UserInterface;
20+
use Symfony\Component\Security\Http\Attribute\CurrentUser;
2021

2122
/**
2223
* Supports the argument type of {@see UserInterface}.
@@ -34,6 +35,10 @@ public function __construct(TokenStorageInterface $tokenStorage)
3435

3536
public function supports(Request $request, ArgumentMetadata $argument): bool
3637
{
38+
if ($argument->getAttribute() instanceof CurrentUser) {
39+
return true;
40+
}
41+
3742
// only security user implementations are supported
3843
if (UserInterface::class !== $argument->getType()) {
3944
return false;

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
2020
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
2121
use Symfony\Component\Security\Core\User\UserInterface;
22+
use Symfony\Component\Security\Http\Attribute\CurrentUser;
2223
use Symfony\Component\Security\Http\Controller\UserValueResolver;
2324

2425
class UserValueResolverTest extends TestCase
@@ -68,6 +69,20 @@ public function testResolve()
6869
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
6970
}
7071

72+
public function testResolveWithAttribute()
73+
{
74+
$user = $this->getMockBuilder(UserInterface::class)->getMock();
75+
$token = new UsernamePasswordToken($user, 'password', 'provider');
76+
$tokenStorage = new TokenStorage();
77+
$tokenStorage->setToken($token);
78+
79+
$resolver = new UserValueResolver($tokenStorage);
80+
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, new CurrentUser());
81+
82+
$this->assertTrue($resolver->supports(Request::create('/'), $metadata));
83+
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
84+
}
85+
7186
public function testIntegration()
7287
{
7388
$user = $this->getMockBuilder(UserInterface::class)->getMock();

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

Lines 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.2",
2222
"symfony/http-foundation": "^4.4.7|^5.0.7",
23-
"symfony/http-kernel": "^4.4|^5.0",
23+
"symfony/http-kernel": "^5.2",
2424
"symfony/polyfill-php80": "^1.15",
2525
"symfony/property-access": "^4.4|^5.0"
2626
},

0 commit comments

Comments
 (0)
0