<?php
/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types=1);
namespace ApiPlatform\Symfony\EventListener;
use ApiPlatform\Api\FormatMatcher;
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
use ApiPlatform\Serializer\SerializerContextBuilderInterface;
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
use ApiPlatform\Symfony\Util\RequestAttributesExtractor;
use ApiPlatform\Symfony\Validator\Exception\ValidationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
use Symfony\Component\Serializer\SerializerInterface;
use Symfony\Component\Validator\Constraints\Type;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Contracts\Translation\LocaleAwareInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Symfony\Contracts\Translation\TranslatorTrait;
/**
* Updates the entity retrieved by the data provider with data contained in the request body.
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class DeserializeListener
{
use OperationRequestInitiatorTrait;
public const OPERATION_ATTRIBUTE_KEY = 'deserialize';
public function __construct(private readonly SerializerInterface $serializer, private readonly SerializerContextBuilderInterface $serializerContextBuilder, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null, private ?TranslatorInterface $translator = null)
{
$this->resourceMetadataCollectionFactory = $resourceMetadataFactory;
if (null === $this->translator) {
$this->translator = new class() implements TranslatorInterface, LocaleAwareInterface {
use TranslatorTrait;
};
$this->translator->setLocale('en');
}
}
/**
* Deserializes the data sent in the requested format.
*
* @throws UnsupportedMediaTypeHttpException
*/
public function onKernelRequest(RequestEvent $event): void
{
$request = $event->getRequest();
$method = $request->getMethod();
if (
'DELETE' === $method
|| $request->isMethodSafe()
|| !($attributes = RequestAttributesExtractor::extractAttributes($request))
|| !$attributes['receive']
|| $request->attributes->get('_api_platform_disable_listeners')
) {
return;
}
$operation = $this->initializeOperation($request);
if ('api_platform.symfony.main_controller' === $operation?->getController()) {
return;
}
if (!($operation?->canDeserialize() ?? true)) {
return;
}
$context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
$format = $this->getFormat($request, $operation?->getInputFormats() ?? []);
$data = $request->attributes->get('data');
if (
null !== $data
&& (
'POST' === $method
|| 'PATCH' === $method
|| ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? false))
)
) {
$context[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
}
try {
$request->attributes->set(
'data',
$this->serializer->deserialize($request->getContent(), $context['resource_class'], $format, $context)
);
} catch (PartialDenormalizationException $e) {
$violations = new ConstraintViolationList();
foreach ($e->getErrors() as $exception) {
if (!$exception instanceof NotNormalizableValueException) {
continue;
}
$message = (new Type($exception->getExpectedTypes() ?? []))->message;
$parameters = [];
if ($exception->canUseMessageForUser()) {
$parameters['hint'] = $exception->getMessage();
}
$violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, Type::INVALID_TYPE_ERROR));
}
if (0 !== \count($violations)) {
throw new ValidationException($violations);
}
}
}
/**
* Extracts the format from the Content-Type header and check that it is supported.
*
* @throws UnsupportedMediaTypeHttpException
*/
private function getFormat(Request $request, array $formats): string
{
/** @var ?string $contentType */
$contentType = $request->headers->get('CONTENT_TYPE');
if (null === $contentType || '' === $contentType) {
throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.');
}
$formatMatcher = new FormatMatcher($formats);
$format = $formatMatcher->getFormat($contentType);
if (null === $format) {
$supportedMimeTypes = [];
foreach ($formats as $mimeTypes) {
foreach ($mimeTypes as $mimeType) {
$supportedMimeTypes[] = $mimeType;
}
}
throw new UnsupportedMediaTypeHttpException(sprintf('The content-type "%s" is not supported. Supported MIME types are "%s".', $contentType, implode('", "', $supportedMimeTypes)));
}
return $format;
}
}