[RFC][HttpKernel][Security] Allowed adding attributes on controller arguments that will be passed to argument resolvers.
This commit is contained in:
parent
d7d479bb9f
commit
20f316906e
@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpKernel\Attribute;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marker interface for controller argument attributes.
|
||||||
|
*/
|
||||||
|
interface ArgumentInterface
|
||||||
|
{
|
||||||
|
}
|
@ -10,6 +10,7 @@ CHANGELOG
|
|||||||
`kernel.trusted_proxies` and `kernel.trusted_headers` parameters
|
`kernel.trusted_proxies` and `kernel.trusted_headers` parameters
|
||||||
* content of request parameter `_password` is now also hidden
|
* content of request parameter `_password` is now also hidden
|
||||||
in the request profiler raw content section
|
in the request profiler raw content section
|
||||||
|
* Allowed adding attributes on controller arguments that will be passed to argument resolvers.
|
||||||
|
|
||||||
5.1.0
|
5.1.0
|
||||||
-----
|
-----
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\HttpKernel\ControllerMetadata;
|
namespace Symfony\Component\HttpKernel\ControllerMetadata;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible for storing metadata of an argument.
|
* Responsible for storing metadata of an argument.
|
||||||
*
|
*
|
||||||
@ -24,8 +26,9 @@ class ArgumentMetadata
|
|||||||
private $hasDefaultValue;
|
private $hasDefaultValue;
|
||||||
private $defaultValue;
|
private $defaultValue;
|
||||||
private $isNullable;
|
private $isNullable;
|
||||||
|
private $attribute;
|
||||||
|
|
||||||
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false)
|
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, ?ArgumentInterface $attribute = null)
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
@ -33,6 +36,7 @@ class ArgumentMetadata
|
|||||||
$this->hasDefaultValue = $hasDefaultValue;
|
$this->hasDefaultValue = $hasDefaultValue;
|
||||||
$this->defaultValue = $defaultValue;
|
$this->defaultValue = $defaultValue;
|
||||||
$this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue);
|
$this->isNullable = $isNullable || null === $type || ($hasDefaultValue && null === $defaultValue);
|
||||||
|
$this->attribute = $attribute;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -104,4 +108,12 @@ class ArgumentMetadata
|
|||||||
|
|
||||||
return $this->defaultValue;
|
return $this->defaultValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the attribute (if any) that was set on the argument.
|
||||||
|
*/
|
||||||
|
public function getAttribute(): ?ArgumentInterface
|
||||||
|
{
|
||||||
|
return $this->attribute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\HttpKernel\ControllerMetadata;
|
namespace Symfony\Component\HttpKernel\ControllerMetadata;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\InvalidMetadataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds {@see ArgumentMetadata} objects based on the given Controller.
|
* Builds {@see ArgumentMetadata} objects based on the given Controller.
|
||||||
*
|
*
|
||||||
@ -34,7 +37,28 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($reflection->getParameters() as $param) {
|
foreach ($reflection->getParameters() as $param) {
|
||||||
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull());
|
$attribute = null;
|
||||||
|
if (method_exists($param, 'getAttributes')) {
|
||||||
|
$reflectionAttributes = $param->getAttributes(ArgumentInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
|
||||||
|
|
||||||
|
if (\count($reflectionAttributes) > 1) {
|
||||||
|
$representative = $controller;
|
||||||
|
|
||||||
|
if (\is_array($representative)) {
|
||||||
|
$representative = sprintf('%s::%s()', \get_class($representative[0]), $representative[1]);
|
||||||
|
} elseif (\is_object($representative)) {
|
||||||
|
$representative = \get_class($representative);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidMetadataException(sprintf('Controller "%s" has more than one attribute for "$%s" argument.', $representative, $param->getName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($reflectionAttributes[0])) {
|
||||||
|
$attribute = $reflectionAttributes[0]->newInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attribute);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $arguments;
|
return $arguments;
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpKernel\Exception;
|
||||||
|
|
||||||
|
class InvalidMetadataException extends \LogicException
|
||||||
|
{
|
||||||
|
}
|
@ -15,6 +15,9 @@ use Fake\ImportedAndFake;
|
|||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
|
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadataFactory;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\InvalidMetadataException;
|
||||||
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
|
||||||
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
|
||||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
|
||||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\NullableController;
|
||||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController;
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\VariadicController;
|
||||||
@ -117,6 +120,28 @@ class ArgumentMetadataFactoryTest extends TestCase
|
|||||||
], $arguments);
|
], $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP 8
|
||||||
|
*/
|
||||||
|
public function testAttributeSignature()
|
||||||
|
{
|
||||||
|
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
|
||||||
|
|
||||||
|
$this->assertEquals([
|
||||||
|
new ArgumentMetadata('baz', 'string', false, false, null, false, new Foo('bar')),
|
||||||
|
], $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP 8
|
||||||
|
*/
|
||||||
|
public function testAttributeSignatureError()
|
||||||
|
{
|
||||||
|
$this->expectException(InvalidMetadataException::class);
|
||||||
|
|
||||||
|
$this->factory->createArgumentMetadata([new AttributeController(), 'invalidAction']);
|
||||||
|
}
|
||||||
|
|
||||||
private function signature1(self $foo, array $bar, callable $baz)
|
private function signature1(self $foo, array $bar, callable $baz)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
||||||
|
|
||||||
|
#[Attribute(Attribute::TARGET_PARAMETER)]
|
||||||
|
class Foo implements ArgumentInterface
|
||||||
|
{
|
||||||
|
private $foo;
|
||||||
|
|
||||||
|
public function __construct($foo)
|
||||||
|
{
|
||||||
|
$this->foo = $foo;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\HttpKernel\Tests\Fixtures\Controller;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
|
||||||
|
|
||||||
|
class AttributeController
|
||||||
|
{
|
||||||
|
public function action(#[Foo('bar')] string $baz) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function invalidAction(#[Foo('bar'), Foo('bar')] string $baz) {
|
||||||
|
}
|
||||||
|
}
|
@ -11,6 +11,7 @@ CHANGELOG
|
|||||||
* 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`
|
* 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`
|
||||||
* Added `FirewallListenerInterface` to make the execution order of firewall listeners configurable
|
* Added `FirewallListenerInterface` to make the execution order of firewall listeners configurable
|
||||||
* Added translator to `\Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator` and `\Symfony\Component\Security\Http\Firewall\UsernamePasswordJsonAuthenticationListener` to translate authentication failure messages
|
* Added translator to `\Symfony\Component\Security\Http\Authenticator\JsonLoginAuthenticator` and `\Symfony\Component\Security\Http\Firewall\UsernamePasswordJsonAuthenticationListener` to translate authentication failure messages
|
||||||
|
* Added a CurrentUser attribute to force the UserValueResolver to resolve an argument to the current user.
|
||||||
|
|
||||||
5.1.0
|
5.1.0
|
||||||
-----
|
-----
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Security\Http\Attribute;
|
||||||
|
|
||||||
|
use Attribute;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates that a controller argument should receive the current logged user.
|
||||||
|
*/
|
||||||
|
#[Attribute(Attribute::TARGET_PARAMETER)]
|
||||||
|
class CurrentUser implements ArgumentInterface
|
||||||
|
{
|
||||||
|
}
|
@ -17,6 +17,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
|||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Supports the argument type of {@see UserInterface}.
|
* Supports the argument type of {@see UserInterface}.
|
||||||
@ -34,6 +35,10 @@ final class UserValueResolver implements ArgumentValueResolverInterface
|
|||||||
|
|
||||||
public function supports(Request $request, ArgumentMetadata $argument): bool
|
public function supports(Request $request, ArgumentMetadata $argument): bool
|
||||||
{
|
{
|
||||||
|
if ($argument->getAttribute() instanceof CurrentUser) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// only security user implementations are supported
|
// only security user implementations are supported
|
||||||
if (UserInterface::class !== $argument->getType()) {
|
if (UserInterface::class !== $argument->getType()) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -19,6 +19,7 @@ use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
|||||||
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage;
|
||||||
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
|
||||||
use Symfony\Component\Security\Core\User\UserInterface;
|
use Symfony\Component\Security\Core\User\UserInterface;
|
||||||
|
use Symfony\Component\Security\Http\Attribute\CurrentUser;
|
||||||
use Symfony\Component\Security\Http\Controller\UserValueResolver;
|
use Symfony\Component\Security\Http\Controller\UserValueResolver;
|
||||||
|
|
||||||
class UserValueResolverTest extends TestCase
|
class UserValueResolverTest extends TestCase
|
||||||
@ -68,6 +69,20 @@ class UserValueResolverTest extends TestCase
|
|||||||
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
|
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testResolveWithAttribute()
|
||||||
|
{
|
||||||
|
$user = $this->getMockBuilder(UserInterface::class)->getMock();
|
||||||
|
$token = new UsernamePasswordToken($user, 'password', 'provider');
|
||||||
|
$tokenStorage = new TokenStorage();
|
||||||
|
$tokenStorage->setToken($token);
|
||||||
|
|
||||||
|
$resolver = new UserValueResolver($tokenStorage);
|
||||||
|
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, new CurrentUser());
|
||||||
|
|
||||||
|
$this->assertTrue($resolver->supports(Request::create('/'), $metadata));
|
||||||
|
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
|
||||||
|
}
|
||||||
|
|
||||||
public function testIntegration()
|
public function testIntegration()
|
||||||
{
|
{
|
||||||
$user = $this->getMockBuilder(UserInterface::class)->getMock();
|
$user = $this->getMockBuilder(UserInterface::class)->getMock();
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"symfony/deprecation-contracts": "^2.1",
|
"symfony/deprecation-contracts": "^2.1",
|
||||||
"symfony/security-core": "^5.2",
|
"symfony/security-core": "^5.2",
|
||||||
"symfony/http-foundation": "^4.4.7|^5.0.7",
|
"symfony/http-foundation": "^4.4.7|^5.0.7",
|
||||||
"symfony/http-kernel": "^4.4|^5.0",
|
"symfony/http-kernel": "^5.2",
|
||||||
"symfony/polyfill-php80": "^1.15",
|
"symfony/polyfill-php80": "^1.15",
|
||||||
"symfony/property-access": "^4.4|^5.0"
|
"symfony/property-access": "^4.4|^5.0"
|
||||||
},
|
},
|
||||||
|
Reference in New Issue
Block a user