[HttpKernel] Handle multi-attribute controller arguments
This commit is contained in:
parent
59fbe57ed1
commit
d771e449ec
@ -44,6 +44,8 @@ HttpFoundation
|
|||||||
HttpKernel
|
HttpKernel
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* Deprecate `ArgumentInterface`
|
||||||
|
* Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead
|
||||||
* Marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
|
* Marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
|
||||||
|
|
||||||
Messenger
|
Messenger
|
||||||
|
@ -92,6 +92,8 @@ HttpFoundation
|
|||||||
HttpKernel
|
HttpKernel
|
||||||
----------
|
----------
|
||||||
|
|
||||||
|
* Remove `ArgumentInterface`
|
||||||
|
* Remove `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead
|
||||||
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
|
* Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+
|
||||||
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
|
* Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead.
|
||||||
|
|
||||||
|
@ -11,8 +11,12 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\HttpKernel\Attribute;
|
namespace Symfony\Component\HttpKernel\Attribute;
|
||||||
|
|
||||||
|
trigger_deprecation('symfony/http-kernel', '5.3', 'The "%s" interface is deprecated.', ArgumentInterface::class);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marker interface for controller argument attributes.
|
* Marker interface for controller argument attributes.
|
||||||
|
*
|
||||||
|
* @deprecated since Symfony 5.3
|
||||||
*/
|
*/
|
||||||
interface ArgumentInterface
|
interface ArgumentInterface
|
||||||
{
|
{
|
||||||
|
@ -4,6 +4,9 @@ CHANGELOG
|
|||||||
5.3
|
5.3
|
||||||
---
|
---
|
||||||
|
|
||||||
|
* Deprecate `ArgumentInterface`
|
||||||
|
* Add `ArgumentMetadata::getAttributes()`
|
||||||
|
* Deprecate `ArgumentMetadata::getAttribute()`, use `getAttributes()` instead
|
||||||
* marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
|
* marked the class `Symfony\Component\HttpKernel\EventListener\DebugHandlersListener` as internal
|
||||||
|
|
||||||
5.2.0
|
5.2.0
|
||||||
|
@ -20,15 +20,20 @@ use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
|||||||
*/
|
*/
|
||||||
class ArgumentMetadata
|
class ArgumentMetadata
|
||||||
{
|
{
|
||||||
|
public const IS_INSTANCEOF = 2;
|
||||||
|
|
||||||
private $name;
|
private $name;
|
||||||
private $type;
|
private $type;
|
||||||
private $isVariadic;
|
private $isVariadic;
|
||||||
private $hasDefaultValue;
|
private $hasDefaultValue;
|
||||||
private $defaultValue;
|
private $defaultValue;
|
||||||
private $isNullable;
|
private $isNullable;
|
||||||
private $attribute;
|
private $attributes;
|
||||||
|
|
||||||
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, ?ArgumentInterface $attribute = null)
|
/**
|
||||||
|
* @param object[] $attributes
|
||||||
|
*/
|
||||||
|
public function __construct(string $name, ?string $type, bool $isVariadic, bool $hasDefaultValue, $defaultValue, bool $isNullable = false, $attributes = [])
|
||||||
{
|
{
|
||||||
$this->name = $name;
|
$this->name = $name;
|
||||||
$this->type = $type;
|
$this->type = $type;
|
||||||
@ -36,7 +41,13 @@ 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;
|
|
||||||
|
if (null === $attributes || $attributes instanceof ArgumentInterface) {
|
||||||
|
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));
|
||||||
|
$attributes = $attributes ? [$attributes] : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->attributes = $attributes;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -114,6 +125,39 @@ class ArgumentMetadata
|
|||||||
*/
|
*/
|
||||||
public function getAttribute(): ?ArgumentInterface
|
public function getAttribute(): ?ArgumentInterface
|
||||||
{
|
{
|
||||||
return $this->attribute;
|
trigger_deprecation('symfony/http-kernel', '5.3', 'Method "%s()" is deprecated, use "getAttributes()" instead.', __METHOD__);
|
||||||
|
|
||||||
|
if (!$this->attributes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->attributes[0] instanceof ArgumentInterface ? $this->attributes[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return object[]
|
||||||
|
*/
|
||||||
|
public function getAttributes(string $name = null, int $flags = 0): array
|
||||||
|
{
|
||||||
|
if (!$name) {
|
||||||
|
return $this->attributes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attributes = [];
|
||||||
|
if ($flags & self::IS_INSTANCEOF) {
|
||||||
|
foreach ($this->attributes as $attribute) {
|
||||||
|
if ($attribute instanceof $name) {
|
||||||
|
$attributes[] = $attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
foreach ($this->attributes as $attribute) {
|
||||||
|
if (\get_class($attribute) === $name) {
|
||||||
|
$attributes[] = $attribute;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attributes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,9 +11,6 @@
|
|||||||
|
|
||||||
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.
|
||||||
*
|
*
|
||||||
@ -37,28 +34,15 @@ final class ArgumentMetadataFactory implements ArgumentMetadataFactoryInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
foreach ($reflection->getParameters() as $param) {
|
foreach ($reflection->getParameters() as $param) {
|
||||||
$attribute = null;
|
|
||||||
if (\PHP_VERSION_ID >= 80000) {
|
if (\PHP_VERSION_ID >= 80000) {
|
||||||
$reflectionAttributes = $param->getAttributes(ArgumentInterface::class, \ReflectionAttribute::IS_INSTANCEOF);
|
foreach ($param->getAttributes() as $reflectionAttribute) {
|
||||||
|
if (class_exists($reflectionAttribute->getName())) {
|
||||||
if (\count($reflectionAttributes) > 1) {
|
$attributes[] = $reflectionAttribute->newInstance();
|
||||||
$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);
|
$arguments[] = new ArgumentMetadata($param->getName(), $this->getType($param, $reflection), $param->isVariadic(), $param->isDefaultValueAvailable(), $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, $param->allowsNull(), $attributes ?? []);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $arguments;
|
return $arguments;
|
||||||
|
@ -15,7 +15,6 @@ 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\Attribute\Foo;
|
||||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\AttributeController;
|
||||||
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Controller\BasicTypesController;
|
||||||
@ -128,18 +127,17 @@ class ArgumentMetadataFactoryTest extends TestCase
|
|||||||
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
|
$arguments = $this->factory->createArgumentMetadata([new AttributeController(), 'action']);
|
||||||
|
|
||||||
$this->assertEquals([
|
$this->assertEquals([
|
||||||
new ArgumentMetadata('baz', 'string', false, false, null, false, new Foo('bar')),
|
new ArgumentMetadata('baz', 'string', false, false, null, false, [new Foo('bar')]),
|
||||||
], $arguments);
|
], $arguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @requires PHP 8
|
* @requires PHP 8
|
||||||
*/
|
*/
|
||||||
public function testAttributeSignatureError()
|
public function testMultipleAttributes()
|
||||||
{
|
{
|
||||||
$this->expectException(InvalidMetadataException::class);
|
$this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg']);
|
||||||
|
$this->assertCount(1, $this->factory->createArgumentMetadata([new AttributeController(), 'multiAttributeArg'])[0]->getAttributes());
|
||||||
$this->factory->createArgumentMetadata([new AttributeController(), 'invalidAction']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function signature1(self $foo, array $bar, callable $baz)
|
private function signature1(self $foo, array $bar, callable $baz)
|
||||||
|
@ -12,10 +12,15 @@
|
|||||||
namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata;
|
namespace Symfony\Component\HttpKernel\Tests\ControllerMetadata;
|
||||||
|
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait;
|
||||||
|
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
||||||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
|
||||||
|
use Symfony\Component\HttpKernel\Tests\Fixtures\Attribute\Foo;
|
||||||
|
|
||||||
class ArgumentMetadataTest extends TestCase
|
class ArgumentMetadataTest extends TestCase
|
||||||
{
|
{
|
||||||
|
use ExpectDeprecationTrait;
|
||||||
|
|
||||||
public function testWithBcLayerWithDefault()
|
public function testWithBcLayerWithDefault()
|
||||||
{
|
{
|
||||||
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value');
|
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value');
|
||||||
@ -41,4 +46,27 @@ class ArgumentMetadataTest extends TestCase
|
|||||||
$this->assertFalse($argument->hasDefaultValue());
|
$this->assertFalse($argument->hasDefaultValue());
|
||||||
$argument->getDefaultValue();
|
$argument->getDefaultValue();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @group legacy
|
||||||
|
*/
|
||||||
|
public function testLegacyAttribute()
|
||||||
|
{
|
||||||
|
$attribute = $this->createMock(ArgumentInterface::class);
|
||||||
|
|
||||||
|
$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.');
|
||||||
|
$this->expectDeprecation('Since symfony/http-kernel 5.3: Method "Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata::getAttribute()" is deprecated, use "getAttributes()" instead.');
|
||||||
|
|
||||||
|
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, $attribute);
|
||||||
|
$this->assertSame($attribute, $argument->getAttribute());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @requires PHP 8
|
||||||
|
*/
|
||||||
|
public function testGetAttributes()
|
||||||
|
{
|
||||||
|
$argument = new ArgumentMetadata('foo', 'string', false, true, 'default value', true, [new Foo('bar')]);
|
||||||
|
$this->assertEquals([new Foo('bar')], $argument->getAttributes());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ namespace Symfony\Component\HttpKernel\Tests\Fixtures\Attribute;
|
|||||||
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
||||||
|
|
||||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||||
class Foo implements ArgumentInterface
|
class Foo
|
||||||
{
|
{
|
||||||
private $foo;
|
private $foo;
|
||||||
|
|
||||||
|
@ -18,6 +18,6 @@ class AttributeController
|
|||||||
public function action(#[Foo('bar')] string $baz) {
|
public function action(#[Foo('bar')] string $baz) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function invalidAction(#[Foo('bar'), Foo('bar')] string $baz) {
|
public function multiAttributeArg(#[Foo('bar'), Undefined('bar')] string $baz) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,12 +11,10 @@
|
|||||||
|
|
||||||
namespace Symfony\Component\Security\Http\Attribute;
|
namespace Symfony\Component\Security\Http\Attribute;
|
||||||
|
|
||||||
use Symfony\Component\HttpKernel\Attribute\ArgumentInterface;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Indicates that a controller argument should receive the current logged user.
|
* Indicates that a controller argument should receive the current logged user.
|
||||||
*/
|
*/
|
||||||
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
#[\Attribute(\Attribute::TARGET_PARAMETER)]
|
||||||
class CurrentUser implements ArgumentInterface
|
class CurrentUser
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@ final class UserValueResolver implements ArgumentValueResolverInterface
|
|||||||
{
|
{
|
||||||
// with the attribute, the type can be any UserInterface implementation
|
// with the attribute, the type can be any UserInterface implementation
|
||||||
// otherwise, the type must be UserInterface
|
// otherwise, the type must be UserInterface
|
||||||
if (UserInterface::class !== $argument->getType() && !$argument->getAttribute() instanceof CurrentUser) {
|
if (UserInterface::class !== $argument->getType() && !$argument->getAttributes(CurrentUser::class, ArgumentMetadata::IS_INSTANCEOF)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,7 +77,8 @@ class UserValueResolverTest extends TestCase
|
|||||||
$tokenStorage->setToken($token);
|
$tokenStorage->setToken($token);
|
||||||
|
|
||||||
$resolver = new UserValueResolver($tokenStorage);
|
$resolver = new UserValueResolver($tokenStorage);
|
||||||
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, new CurrentUser());
|
$metadata = $this->createMock(ArgumentMetadata::class);
|
||||||
|
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, [new CurrentUser()]);
|
||||||
|
|
||||||
$this->assertTrue($resolver->supports(Request::create('/'), $metadata));
|
$this->assertTrue($resolver->supports(Request::create('/'), $metadata));
|
||||||
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
|
$this->assertSame([$user], iterator_to_array($resolver->resolve(Request::create('/'), $metadata)));
|
||||||
@ -89,7 +90,7 @@ class UserValueResolverTest extends TestCase
|
|||||||
$tokenStorage->setToken(new UsernamePasswordToken('username', 'password', 'provider'));
|
$tokenStorage->setToken(new UsernamePasswordToken('username', 'password', 'provider'));
|
||||||
|
|
||||||
$resolver = new UserValueResolver($tokenStorage);
|
$resolver = new UserValueResolver($tokenStorage);
|
||||||
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, new CurrentUser());
|
$metadata = new ArgumentMetadata('foo', null, false, false, null, false, [new CurrentUser()]);
|
||||||
|
|
||||||
$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
|
$this->assertFalse($resolver->supports(Request::create('/'), $metadata));
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
"symfony/deprecation-contracts": "^2.1",
|
"symfony/deprecation-contracts": "^2.1",
|
||||||
"symfony/security-core": "^5.3",
|
"symfony/security-core": "^5.3",
|
||||||
"symfony/http-foundation": "^5.2",
|
"symfony/http-foundation": "^5.2",
|
||||||
"symfony/http-kernel": "^5.2",
|
"symfony/http-kernel": "^5.3",
|
||||||
"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