-
Notifications
You must be signed in to change notification settings - Fork 702
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Create a controller argument resolver to replace the param converter
- Loading branch information
Showing
3 changed files
with
301 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the FOSRestBundle package. | ||
* | ||
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace FOS\RestBundle\Controller\Annotations; | ||
|
||
use FOS\RestBundle\Controller\ArgumentResolver\RequestBodyValueResolver; | ||
use Symfony\Component\HttpKernel\Attribute\ValueResolver; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
|
||
if (class_exists(ValueResolver::class)) { | ||
/** | ||
* Compat value resolver for Symfony 6.3 and newer. | ||
* | ||
* @internal | ||
*/ | ||
abstract class CompatMapRequestBody extends ValueResolver {} | ||
} else { | ||
/** | ||
* Compat value resolver for Symfony 6.2 and older. | ||
* | ||
* @internal | ||
*/ | ||
abstract class CompatMapRequestBody | ||
{ | ||
public function __construct(string $resolver) | ||
{ | ||
// No-op'd constructor because the ValueResolver does not exist on this Symfony version | ||
} | ||
} | ||
} | ||
|
||
#[\Attribute(\Attribute::TARGET_PARAMETER)] | ||
final class MapRequestBody extends CompatMapRequestBody | ||
{ | ||
/** | ||
* @var ArgumentMetadata|null | ||
*/ | ||
public $metadata = null; | ||
|
||
/** | ||
* @var array<string, mixed> | ||
*/ | ||
public $deserializationContext; | ||
|
||
/** | ||
* @var bool | ||
*/ | ||
public $validate; | ||
|
||
/** | ||
* @var array<string, mixed> | ||
*/ | ||
public $validator; | ||
|
||
/** | ||
* @param array<string, mixed> $deserializationContext | ||
* @param array<string, mixed> $validator | ||
*/ | ||
public function __construct( | ||
array $deserializationContext = [], | ||
bool $validate = false, | ||
array $validator = [], | ||
string $resolver = RequestBodyValueResolver::class, | ||
) { | ||
$this->deserializationContext = $deserializationContext; | ||
$this->validate = $validate; | ||
$this->validator = $validator; | ||
|
||
parent::__construct($resolver); | ||
} | ||
} |
219 changes: 219 additions & 0 deletions
219
Controller/ArgumentResolver/RequestBodyValueResolver.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,219 @@ | ||
<?php | ||
|
||
/* | ||
* This file is part of the FOSRestBundle package. | ||
* | ||
* (c) FriendsOfSymfony <http://friendsofsymfony.github.com/> | ||
* | ||
* For the full copyright and license information, please view the LICENSE | ||
* file that was distributed with this source code. | ||
*/ | ||
|
||
namespace FOS\RestBundle\Controller\ArgumentResolver; | ||
|
||
use FOS\RestBundle\Context\Context; | ||
use FOS\RestBundle\Controller\Annotations\MapRequestBody; | ||
use FOS\RestBundle\Serializer\Serializer; | ||
use JMS\Serializer\Exception\Exception as JMSSerializerException; | ||
use JMS\Serializer\Exception\UnsupportedFormatException; | ||
use Symfony\Component\EventDispatcher\EventSubscriberInterface; | ||
use Symfony\Component\HttpFoundation\Request; | ||
use Symfony\Component\HttpKernel\Controller\ArgumentValueResolverInterface; | ||
use Symfony\Component\HttpKernel\Controller\ValueResolverInterface; | ||
use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; | ||
use Symfony\Component\HttpKernel\Event\ControllerArgumentsEvent; | ||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; | ||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException; | ||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException; | ||
use Symfony\Component\HttpKernel\KernelEvents; | ||
use Symfony\Component\OptionsResolver\OptionsResolver; | ||
use Symfony\Component\Serializer\Exception\ExceptionInterface as SymfonySerializerException; | ||
use Symfony\Component\Validator\Exception\ValidationFailedException; | ||
use Symfony\Component\Validator\Validator\ValidatorInterface; | ||
|
||
if (interface_exists(ValueResolverInterface::class)) { | ||
/** | ||
* Compat value resolver for Symfony 6.2 and newer. | ||
* | ||
* @internal | ||
*/ | ||
abstract class CompatRequestBodyValueResolver implements ValueResolverInterface {} | ||
} else { | ||
/** | ||
* Compat value resolver for Symfony 6.1 and older. | ||
* | ||
* @internal | ||
*/ | ||
abstract class CompatRequestBodyValueResolver implements ArgumentValueResolverInterface | ||
{ | ||
public function supports(Request $request, ArgumentMetadata $argument): bool | ||
{ | ||
$attribute = $argument->getAttributesOfType(MapRequestBody::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null; | ||
|
||
return $attribute instanceof MapRequestBody; | ||
} | ||
} | ||
} | ||
|
||
final class RequestBodyValueResolver extends CompatRequestBodyValueResolver implements EventSubscriberInterface | ||
{ | ||
/** | ||
* @var Serializer | ||
*/ | ||
private $serializer; | ||
|
||
/** | ||
* @var array<string, mixed> | ||
*/ | ||
private $context = []; | ||
|
||
/** | ||
* @var ValidatorInterface|null | ||
*/ | ||
private $validator; | ||
|
||
/** | ||
* @param list<string>|null $groups | ||
*/ | ||
public function __construct( | ||
Serializer $serializer, | ||
?array $groups = null, | ||
?string $version = null, | ||
?ValidatorInterface $validator = null | ||
) { | ||
$this->serializer = $serializer; | ||
$this->validator = $validator; | ||
|
||
if (!empty($groups)) { | ||
$this->context['groups'] = (array) $groups; | ||
} | ||
|
||
if (!empty($version)) { | ||
$this->context['version'] = $version; | ||
} | ||
} | ||
|
||
public static function getSubscribedEvents(): array | ||
{ | ||
return [ | ||
KernelEvents::CONTROLLER_ARGUMENTS => 'onKernelControllerArguments', | ||
]; | ||
} | ||
|
||
public function resolve(Request $request, ArgumentMetadata $argument): iterable | ||
{ | ||
$attribute = $argument->getAttributesOfType(MapRequestBody::class, ArgumentMetadata::IS_INSTANCEOF)[0] ?? null; | ||
|
||
if (!$attribute) { | ||
return []; | ||
} | ||
|
||
if ($argument->isVariadic()) { | ||
throw new \LogicException(sprintf('Mapping variadic argument "$%s" is not supported.', $argument->getName())); | ||
} | ||
|
||
$attribute->metadata = $argument; | ||
|
||
return [$attribute]; | ||
} | ||
|
||
public function onKernelControllerArguments(ControllerArgumentsEvent $event): void | ||
{ | ||
$arguments = $event->getArguments(); | ||
|
||
foreach ($arguments as $i => $argument) { | ||
if (!$argument instanceof MapRequestBody) { | ||
continue; | ||
} | ||
|
||
if (!$type = $argument->metadata->getType()) { | ||
throw new \LogicException(sprintf('Could not resolve the "$%s" controller argument: argument should be typed.', $argument->metadata->getName())); | ||
} | ||
|
||
$request = $event->getRequest(); | ||
|
||
$format = method_exists(Request::class, 'getContentTypeFormat') ? $request->getContentTypeFormat() : $request->getContentType(); | ||
|
||
if (null === $format) { | ||
throw new UnsupportedMediaTypeHttpException('Unsupported format.'); | ||
} | ||
|
||
try { | ||
$payload = $this->serializer->deserialize( | ||
$request->getContent(), | ||
$type, | ||
$format, | ||
$this->createContext(array_merge($this->context, $argument->deserializationContext)) | ||
); | ||
} catch (UnsupportedFormatException $e) { | ||
throw new UnsupportedMediaTypeHttpException($e->getMessage(), $e); | ||
} catch (JMSSerializerException|SymfonySerializerException $e) { | ||
throw new BadRequestHttpException($e->getMessage(), $e); | ||
} | ||
|
||
if (null !== $payload && null !== $this->validator && $argument->validate) { | ||
$validatorOptions = $this->getValidatorOptions($argument); | ||
|
||
$violations = $this->validator->validate($payload, null, $validatorOptions['groups']); | ||
|
||
if (\count($violations)) { | ||
throw new UnprocessableEntityHttpException( | ||
implode("\n", array_map(static function ($e) { return $e->getMessage(); }, iterator_to_array($violations))), | ||
new ValidationFailedException($payload, $violations) | ||
); | ||
} | ||
} | ||
|
||
if (null === $payload) { | ||
if ($argument->metadata->hasDefaultValue()) { | ||
$payload = $argument->metadata->getDefaultValue(); | ||
} elseif ($argument->metadata->isNullable()) { | ||
$payload = null; | ||
} else { | ||
throw new UnprocessableEntityHttpException(); | ||
} | ||
} | ||
|
||
$arguments[$i] = $payload; | ||
} | ||
|
||
$event->setArguments($arguments); | ||
} | ||
|
||
private function createContext(array $options): Context | ||
{ | ||
$context = new Context(); | ||
|
||
foreach ($options as $key => $value) { | ||
if ('groups' === $key) { | ||
$context->addGroups($options['groups']); | ||
} elseif ('version' === $key) { | ||
$context->setVersion($options['version']); | ||
} elseif ('enableMaxDepth' === $key) { | ||
if (true === $options['enableMaxDepth']) { | ||
$context->enableMaxDepth(); | ||
} elseif (false === $options['enableMaxDepth']) { | ||
$context->disableMaxDepth(); | ||
} | ||
} elseif ('serializeNull' === $key) { | ||
$context->setSerializeNull($options['serializeNull']); | ||
} else { | ||
$context->setAttribute($key, $value); | ||
} | ||
} | ||
|
||
return $context; | ||
} | ||
|
||
private function getValidatorOptions(MapRequestBody $argument): array | ||
{ | ||
$resolver = new OptionsResolver(); | ||
$resolver->setDefaults([ | ||
'groups' => null, | ||
'traverse' => false, | ||
'deep' => false, | ||
]); | ||
|
||
return $resolver->resolve($argument->validator); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters