Skip to content

Commit a8f5223

Browse files
committed
Create a controller argument resolver to replace the param converter
1 parent e01be81 commit a8f5223

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\RestBundle\Controller\Annotations;
13+
14+
use FOS\RestBundle\Controller\ArgumentResolver\RequestBodyValueResolver;
15+
use Symfony\Component\HttpKernel\Attribute\ValueResolver;
16+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
17+
18+
if (class_exists(ValueResolver::class)) {
19+
/**
20+
* Compat value resolver for Symfony 6.3 and newer.
21+
*
22+
* @internal
23+
*/
24+
abstract class CompatMapRequestBody extends ValueResolver {}
25+
} else {
26+
/**
27+
* Compat value resolver for Symfony 6.2 and older.
28+
*
29+
* @internal
30+
*/
31+
abstract class CompatMapRequestBody
32+
{
33+
public function __construct(string $resolver)
34+
{
35+
// No-op'd constructor because the ValueResolver does not exist on this Symfony version
36+
}
37+
}
38+
}
39+
40+
#[\Attribute(\Attribute::TARGET_PARAMETER)]
41+
final class MapRequestBody extends CompatMapRequestBody
42+
{
43+
/**
44+
* @var ArgumentMetadata|null
45+
*/
46+
public $metadata = null;
47+
48+
/**
49+
* @var array<string, mixed>
50+
*/
51+
public $deserializationContext;
52+
53+
/**
54+
* @var bool
55+
*/
56+
public $validate;
57+
58+
/**
59+
* @var array<string, mixed>
60+
*/
61+
public $validator;
62+
63+
/**
64+
* @param array<string, mixed> $deserializationContext
65+
* @param array<string, mixed> $validator
66+
*/
67+
public function __construct(
68+
array $deserializationContext = [],
69+
bool $validate = false,
70+
array $validator = [],
71+
string $resolver = RequestBodyValueResolver::class,
72+
) {
73+
$this->deserializationContext = $deserializationContext;
74+
$this->validate = $validate;
75+
$this->validator = $validator;
76+
77+
parent::__construct($resolver);
78+
}
79+
}
Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the FOSRestBundle package.
5+
*
6+
* (c) FriendsOfSymfony <http://friendsofsymfony.github.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 FOS\RestBundle\Controller\ArgumentResolver;
13+
14+
use FOS\RestBundle\Context\Context;
15+
use FOS\RestBundle\Controller\Annotations\MapRequestBody;
16+
use FOS\RestBundle\Serializer\Serializer;
17+
use JMS\Serializer\Exception\Exception as JMSSerializerException;
18+
use JMS\Serializer\Exception\UnsupportedFormatException;
19+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
20+
use Symfony\Component\HttpFoundation\Request;
21+
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface;
22+
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface;
23+
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata;
24+
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent;
25+
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
26+
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
27+
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
28+
use Symfony\Component\HttpKernel\KernelEvents;
29+
use Symfony\Component\OptionsResolver\OptionsResolver;
30+
use Symfony\Component\Serializer\Exception\ExceptionInterface as SymfonySerializerException;
31+
use Symfony\Component\Validator\Exception\ValidationFailedException;
32+
use Symfony\Component\Validator\Validator\ValidatorInterface;
33+
34+
if (interface_exists(ValueResolverInterface::class)) {
35+
/**
36+
* Compat value resolver for Symfony 6.2 and newer.
37+
*
38+
* @internal
39+
*/
40+
abstract class CompatRequestBodyValueResolver implements ValueResolverInterface {}
41+
} else {
42+
/**
43+
* Compat value resolver for Symfony 6.1 and older.
44+
*
45+
* @internal
46+
*/
47+
abstract class CompatRequestBodyValueResolver implements ArgumentValueResolverInterface
48+
{
49+
public function supports(Request $request, ArgumentMetadata $argument): bool
50+
{
51+
$attribute = $argument->getAttributesOfType(MapRequestBody::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null;
52+
53+
return $attribute instanceof MapRequestBody;
54+
}
55+
}
56+
}
57+
58+
final class RequestBodyValueResolver extends CompatRequestBodyValueResolver implements EventSubscriberInterface
59+
{
60+
/**
61+
* @var Serializer
62+
*/
63+
private $serializer;
64+
65+
/**
66+
* @var array<string, mixed>
67+
*/
68+
private $context = [];
69+
70+
/**
71+
* @var ValidatorInterface|null
72+
*/
73+
private $validator;
74+
75+
/**
76+
* @param list<string>|null $groups
77+
*/
78+
public function __construct(
79+
Serializer $serializer,
80+
?array $groups = null,
81+
?string $version = null,
82+
?ValidatorInterface $validator = null
83+
) {
84+
$this->serializer = $serializer;
85+
$this->validator = $validator;
86+
87+
if (!empty($groups)) {
88+
$this->context['groups'] = (array) $groups;
89+
}
90+
91+
if (!empty($version)) {
92+
$this->context['version'] = $version;
93+
}
94+
}
95+
96+
public static function getSubscribedEvents(): array
97+
{
98+
return [
99+
KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments',
100+
];
101+
}
102+
103+
public function resolve(Request $request, ArgumentMetadata $argument): iterable
104+
{
105+
$attribute = $argument->getAttributesOfType(MapRequestBody::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null;
106+
107+
if (!$attribute) {
108+
return [];
109+
}
110+
111+
if ($argument->isVariadic()) {
112+
throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName()));
113+
}
114+
115+
$attribute->metadata = $argument;
116+
117+
return [$attribute];
118+
}
119+
120+
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void
121+
{
122+
$arguments = $event->getArguments();
123+
124+
foreach ($arguments as $i => $argument) {
125+
if (!$argument instanceof MapRequestBody) {
126+
continue;
127+
}
128+
129+
if (!$type = $argument->metadata->getType()) {
130+
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName()));
131+
}
132+
133+
$request = $event->getRequest();
134+
135+
$format = method_exists(Request::class, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType();
136+
137+
if (null === $format) {
138+
throw new UnsupportedMediaTypeHttpException('Unsupported format.');
139+
}
140+
141+
try {
142+
$payload = $this->serializer->deserialize(
143+
$request->getContent(),
144+
$type,
145+
$format,
146+
$this->createContext(array_merge($this->context, $argument->deserializationContext))
147+
);
148+
} catch (UnsupportedFormatException $e) {
149+
throw new UnsupportedMediaTypeHttpException($e->getMessage(), $e);
150+
} catch (JMSSerializerException|SymfonySerializerException $e) {
151+
throw new BadRequestHttpException($e->getMessage(), $e);
152+
}
153+
154+
if (null !== $payload && null !== $this->validator && $argument->validate) {
155+
$validatorOptions = $this->getValidatorOptions($argument);
156+
157+
$violations = $this->validator->validate($payload, null, $validatorOptions['groups']);
158+
159+
if (\count($violations)) {
160+
throw new UnprocessableEntityHttpException(
161+
implode("\n", array_map(static function ($e) { return $e->getMessage(); }, iterator_to_array($violations))),
162+
new ValidationFailedException($payload, $violations)
163+
);
164+
}
165+
}
166+
167+
if (null === $payload) {
168+
if ($argument->metadata->hasDefaultValue()) {
169+
$payload = $argument->metadata->getDefaultValue();
170+
} elseif ($argument->metadata->isNullable()) {
171+
$payload = null;
172+
} else {
173+
throw new UnprocessableEntityHttpException();
174+
}
175+
}
176+
177+
$arguments[$i] = $payload;
178+
}
179+
180+
$event->setArguments($arguments);
181+
}
182+
183+
private function createContext(array $options): Context
184+
{
185+
$context = new Context();
186+
187+
foreach ($options as $key => $value) {
188+
if ('groups' === $key) {
189+
$context->addGroups($options['groups']);
190+
} elseif ('version' === $key) {
191+
$context->setVersion($options['version']);
192+
} elseif ('enableMaxDepth' === $key) {
193+
if (true === $options['enableMaxDepth']) {
194+
$context->enableMaxDepth();
195+
} elseif (false === $options['enableMaxDepth']) {
196+
$context->disableMaxDepth();
197+
}
198+
} elseif ('serializeNull' === $key) {
199+
$context->setSerializeNull($options['serializeNull']);
200+
} else {
201+
$context->setAttribute($key, $value);
202+
}
203+
}
204+
205+
return $context;
206+
}
207+
208+
private function getValidatorOptions(MapRequestBody $argument): array
209+
{
210+
$resolver = new OptionsResolver();
211+
$resolver->setDefaults([
212+
'groups' => null,
213+
'traverse' => false,
214+
'deep' => false,
215+
]);
216+
217+
return $resolver->resolve($argument->validator);
218+
}
219+
}

Request/RequestBodyParamConverter.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
namespace FOS\RestBundle\Request;
1313

1414
use FOS\RestBundle\Context\Context;
15+
use FOS\RestBundle\Controller\ArgumentResolver\RequestBodyValueResolver;
1516
use FOS\RestBundle\Serializer\Serializer;
1617
use JMS\Serializer\Exception\Exception as JMSSerializerException;
1718
use JMS\Serializer\Exception\UnsupportedFormatException;
@@ -26,6 +27,8 @@
2627

2728
/**
2829
* @author Tyler Stroud <[email protected]>
30+
*
31+
* @deprecated use {@see RequestBodyValueResolver} instead
2932
*/
3033
final class RequestBodyParamConverter implements ParamConverterInterface
3134
{

0 commit comments

Comments
 (0)