Skip to content

Commit 5c32a49

Browse files
committed
[HttpKernel] Add the UidValueResolver argument value resolver
1 parent 2159c36 commit 5c32a49

File tree

4 files changed

+169
-0
lines changed

4 files changed

+169
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ CHANGELOG
77
* Add `BackedEnumValueResolver` to resolve backed enum cases from request attributes in controller arguments
88
* Deprecate StreamedResponseListener, it's not needed anymore
99
* Add `Profiler::isEnabled()` so collaborating collector services may elect to omit themselves.
10+
* Add the `UidValueResolver` argument value resolver
1011

1112
6.0
1213
---
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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\Controller\ArgumentResolver;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
18+
use Symfony\Component\Uid\AbstractUid;
19+
20+
final class UidValueResolver implements ArgumentValueResolverInterface
21+
{
22+
/**
23+
* {@inheritdoc}
24+
*/
25+
public function supports(Request $request, ArgumentMetadata $argument): bool
26+
{
27+
return !$argument->isVariadic()
28+
&& \is_string($request->attributes->get($argument->getName()))
29+
&& null !== $argument->getType()
30+
&& is_subclass_of($argument->getType(), AbstractUid::class, true);
31+
}
32+
33+
/**
34+
* {@inheritdoc}
35+
*/
36+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
37+
{
38+
/** @var class-string<AbstractUid> $uidClass */
39+
$uidClass = $argument->getType();
40+
41+
try {
42+
return [$uidClass::fromString($request->attributes->get($argument->getName()))];
43+
} catch (\InvalidArgumentException $e) {
44+
throw new NotFoundHttpException(sprintf('The uid for the "%s" parameter is invalid.', $argument->getName()), $e);
45+
}
46+
}
47+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
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\Controller\ArgumentResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\HttpFoundation\Request;
16+
use Symfony\Component\HttpKernel\Controller\ArgumentResolver\UidValueResolver;
17+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
18+
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
19+
use Symfony\Component\Uid\AbstractUid;
20+
use Symfony\Component\Uid\Factory\UlidFactory;
21+
use Symfony\Component\Uid\Ulid;
22+
use Symfony\Component\Uid\UuidV1;
23+
use Symfony\Component\Uid\UuidV4;
24+
25+
class UidValueResolverTest extends TestCase
26+
{
27+
/**
28+
* @dataProvider provideSupports
29+
*/
30+
public function testSupports(bool $expected, Request $request, ArgumentMetadata $argument)
31+
{
32+
$this->assertSame($expected, (new UidValueResolver())->supports($request, $argument));
33+
}
34+
35+
public function provideSupports()
36+
{
37+
return [
38+
'Variadic argument' => [false, new Request([], [], ['foo' => (string) $uuidV4 = new UuidV4()]), new ArgumentMetadata('foo', UuidV4::class, true, false, null)],
39+
'No attribute for argument' => [false, new Request([], [], []), new ArgumentMetadata('foo', UuidV4::class, false, false, null)],
40+
'Attribute is not a string' => [false, new Request([], [], ['foo' => ['bar']]), new ArgumentMetadata('foo', UuidV4::class, false, false, null)],
41+
'Argument has no type' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', null, false, false, null)],
42+
'Argument type is not a class' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', 'string', false, false, null)],
43+
'Argument type is not a subclass of AbstractUid' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UlidFactory::class, false, false, null)],
44+
'AbstractUid is not supported' => [false, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', AbstractUid::class, false, false, null)],
45+
'Custom abstract subclass is supported but will fail in resolve' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', TestAbstractCustomUid::class, false, false, null)],
46+
'Known subclass' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', UuidV4::class, false, false, null)],
47+
'Format does not matter' => [true, new Request([], [], ['foo' => (string) $uuidV4]), new ArgumentMetadata('foo', Ulid::class, false, false, null)],
48+
'Custom subclass' => [true, new Request([], [], ['foo' => '01FPND7BD15ZV07X5VGDXAJ8VD']), new ArgumentMetadata('foo', TestCustomUid::class, false, false, null)],
49+
];
50+
}
51+
52+
/**
53+
* @dataProvider provideResolveOK
54+
*/
55+
public function testResolveOK(AbstractUid $expected, string $requestUid)
56+
{
57+
$this->assertEquals([$expected], (new UidValueResolver())->resolve(
58+
new Request([], [], ['id' => $requestUid]),
59+
new ArgumentMetadata('id', \get_class($expected), false, false, null)
60+
));
61+
}
62+
63+
public function provideResolveOK()
64+
{
65+
return [
66+
[$uuidV1 = new UuidV1(), (string) $uuidV1],
67+
[$uuidV1, $uuidV1->toBase58()],
68+
[$uuidV1, $uuidV1->toBase32()],
69+
[$ulid = Ulid::fromBase32('01FQC6Y03WDZ73DQY9RXQMPHB1'), (string) $ulid],
70+
[$ulid, $ulid->toBase58()],
71+
[$ulid, $ulid->toRfc4122()],
72+
[$customUid = new TestCustomUid(), (string) $customUid],
73+
[$customUid, $customUid->toBase58()],
74+
[$customUid, $customUid->toBase32()],
75+
];
76+
}
77+
78+
/**
79+
* @dataProvider provideResolveKO
80+
*/
81+
public function testResolveKO(string $requestUid, string $argumentType)
82+
{
83+
$this->expectException(NotFoundHttpException::class);
84+
$this->expectExceptionMessage('The uid for the "id" parameter is invalid.');
85+
86+
(new UidValueResolver())->resolve(
87+
new Request([], [], ['id' => $requestUid]),
88+
new ArgumentMetadata('id', $argumentType, false, false, null)
89+
);
90+
}
91+
92+
public function provideResolveKO()
93+
{
94+
return [
95+
'Bad value for UUID' => ['ccc', UuidV1::class],
96+
'Bad value for ULID' => ['ccc', Ulid::class],
97+
'Bad value for custom UID' => ['ccc', TestCustomUid::class],
98+
'Bad UUID version' => [(string) new UuidV4(), UuidV1::class],
99+
];
100+
}
101+
102+
public function testResolveAbstractClass()
103+
{
104+
$this->expectException(\Error::class);
105+
$this->expectExceptionMessage('Cannot instantiate abstract class Symfony\Component\HttpKernel\Tests\Controller\ArgumentResolver\TestAbstractCustomUid');
106+
107+
(new UidValueResolver())->resolve(
108+
new Request([], [], ['id' => (string) new UuidV1()]),
109+
new ArgumentMetadata('id', TestAbstractCustomUid::class, false, false, null)
110+
);
111+
}
112+
}
113+
114+
class TestCustomUid extends UuidV1
115+
{
116+
}
117+
118+
abstract class TestAbstractCustomUid extends UuidV1
119+
{
120+
}

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"symfony/stopwatch": "^5.4|^6.0",
3939
"symfony/translation": "^5.4|^6.0",
4040
"symfony/translation-contracts": "^1.1|^2|^3",
41+
"symfony/uid": "^5.4|^6.0",
4142
"psr/cache": "^1.0|^2.0|^3.0",
4243
"twig/twig": "^2.13|^3.0.4"
4344
},

0 commit comments

Comments
 (0)