• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

api-platform / core / 10682869832

03 Sep 2024 12:24PM UTC coverage: 70.804%. Remained the same
10682869832

push

github

soyuka
docs: changelog v3.4.0-alpha.4

3075 of 4343 relevant lines covered (70.8%)

75.56 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.08
/src/Symfony/EventListener/DeserializeListener.php
1
<?php
2

3
/*
4
 * This file is part of the API Platform project.
5
 *
6
 * (c) Kévin Dunglas <dunglas@gmail.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
declare(strict_types=1);
13

14
namespace ApiPlatform\Symfony\EventListener;
15

16
use ApiPlatform\Api\FormatMatcher;
17
use ApiPlatform\Metadata\HttpOperation;
18
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
19
use ApiPlatform\Serializer\SerializerContextBuilderInterface as LegacySerializerContextBuilderInterface;
20
use ApiPlatform\State\ProviderInterface;
21
use ApiPlatform\State\SerializerContextBuilderInterface;
22
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
23
use ApiPlatform\Symfony\Util\RequestAttributesExtractor;
24
use ApiPlatform\Symfony\Validator\Exception\ValidationException;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpKernel\Event\RequestEvent;
27
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
28
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
29
use Symfony\Component\Serializer\Exception\PartialDenormalizationException;
30
use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
31
use Symfony\Component\Serializer\SerializerInterface;
32
use Symfony\Component\Validator\Constraints\Type;
33
use Symfony\Component\Validator\ConstraintViolation;
34
use Symfony\Component\Validator\ConstraintViolationList;
35
use Symfony\Contracts\Translation\LocaleAwareInterface;
36
use Symfony\Contracts\Translation\TranslatorInterface;
37
use Symfony\Contracts\Translation\TranslatorTrait;
38

39
/**
40
 * Updates the entity retrieved by the data provider with data contained in the request body.
41
 *
42
 * @author Kévin Dunglas <dunglas@gmail.com>
43
 */
44
final class DeserializeListener
45
{
46
    use OperationRequestInitiatorTrait;
47

48
    public const OPERATION_ATTRIBUTE_KEY = 'deserialize';
49
    private SerializerInterface $serializer;
50
    private ?ProviderInterface $provider = null;
51

52
    public function __construct(
53
        ProviderInterface|SerializerInterface $serializer,
54
        private readonly LegacySerializerContextBuilderInterface|SerializerContextBuilderInterface|ResourceMetadataCollectionFactoryInterface|null $serializerContextBuilder = null,
55
        ?ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory = null,
56
        private ?TranslatorInterface $translator = null,
57
    ) {
58
        if ($serializer instanceof ProviderInterface) {
166✔
59
            $this->provider = $serializer;
×
60
        } else {
61
            trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProviderInterface::class, self::class, SerializerInterface::class);
166✔
62
            $this->serializer = $serializer;
166✔
63
        }
64

65
        if ($serializerContextBuilder instanceof ResourceMetadataCollectionFactoryInterface) {
166✔
66
            $resourceMetadataFactory = $serializerContextBuilder;
×
67
        } else {
68
            trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as second argument in "%s" instead of "%s".', ResourceMetadataCollectionFactoryInterface::class, self::class, SerializerContextBuilderInterface::class);
166✔
69
        }
70

71
        $this->resourceMetadataCollectionFactory = $resourceMetadataFactory;
166✔
72
        if (null === $this->translator) {
166✔
73
            $this->translator = new class implements TranslatorInterface, LocaleAwareInterface {
48✔
74
                use TranslatorTrait;
75
            };
48✔
76
            $this->translator->setLocale('en');
48✔
77
        }
78
    }
79

80
    /**
81
     * Deserializes the data sent in the requested format.
82
     *
83
     * @throws UnsupportedMediaTypeHttpException
84
     */
85
    public function onKernelRequest(RequestEvent $event): void
86
    {
87
        $request = $event->getRequest();
166✔
88
        $method = $request->getMethod();
166✔
89

90
        if (
91
            !($attributes = RequestAttributesExtractor::extractAttributes($request))
166✔
92
            || !$attributes['receive']
166✔
93
        ) {
94
            return;
33✔
95
        }
96

97
        $operation = $this->initializeOperation($request);
136✔
98

99
        if ($operation && $this->provider) {
136✔
100
            if (null === $operation->canDeserialize() && $operation instanceof HttpOperation) {
×
101
                $operation = $operation->withDeserialize(\in_array($operation->getMethod(), ['POST', 'PUT', 'PATCH'], true));
×
102
            }
103

104
            if (!$operation->canDeserialize()) {
×
105
                return;
×
106
            }
107

108
            $data = $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [
×
109
                'request' => $request,
×
110
                'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [],
×
111
                'resource_class' => $operation->getClass(),
×
112
            ]);
×
113

114
            $request->attributes->set('data', $data);
×
115

116
            return;
×
117
        }
118

119
        // TODO: the code below needs to be removed in 4.x
120
        if (
121
            'DELETE' === $method
136✔
122
            || $request->isMethodSafe()
136✔
123
            || $request->attributes->get('_api_platform_disable_listeners')
136✔
124
        ) {
125
            return;
93✔
126
        }
127

128
        if ('api_platform.symfony.main_controller' === $operation?->getController()) {
43✔
129
            return;
3✔
130
        }
131

132
        if (!($operation?->canDeserialize() ?? true)) {
40✔
133
            return;
4✔
134
        }
135

136
        $context = $this->serializerContextBuilder->createFromRequest($request, false, $attributes);
36✔
137

138
        $format = $this->getFormat($request, $operation?->getInputFormats() ?? []);
36✔
139
        $data = $request->attributes->get('data');
24✔
140
        if (
141
            null !== $data
24✔
142
            && (
143
                'POST' === $method
24✔
144
                || 'PATCH' === $method
24✔
145
                || ('PUT' === $method && !($operation->getExtraProperties()['standard_put'] ?? false))
24✔
146
            )
147
        ) {
148
            $context[AbstractNormalizer::OBJECT_TO_POPULATE] = $data;
8✔
149
        }
150
        try {
151
            $request->attributes->set(
24✔
152
                'data',
24✔
153
                $this->serializer->deserialize($request->getContent(), $context['resource_class'], $format, $context)
24✔
154
            );
24✔
155
        } catch (PartialDenormalizationException $e) {
4✔
156
            $violations = new ConstraintViolationList();
4✔
157
            foreach ($e->getErrors() as $exception) {
4✔
158
                if (!$exception instanceof NotNormalizableValueException) {
4✔
159
                    continue;
×
160
                }
161
                $message = (new Type($exception->getExpectedTypes() ?? []))->message;
4✔
162
                $parameters = [];
4✔
163
                if ($exception->canUseMessageForUser()) {
4✔
164
                    $parameters['hint'] = $exception->getMessage();
4✔
165
                }
166
                $violations->add(new ConstraintViolation($this->translator->trans($message, ['{{ type }}' => implode('|', $exception->getExpectedTypes() ?? [])], 'validators'), $message, $parameters, null, $exception->getPath(), null, null, Type::INVALID_TYPE_ERROR));
4✔
167
            }
168
            if (0 !== \count($violations)) {
4✔
169
                throw new ValidationException($violations);
4✔
170
            }
171
        }
172
    }
173

174
    /**
175
     * Extracts the format from the Content-Type header and check that it is supported.
176
     *
177
     * @throws UnsupportedMediaTypeHttpException
178
     */
179
    private function getFormat(Request $request, array $formats): string
180
    {
181
        /** @var ?string $contentType */
182
        $contentType = $request->headers->get('CONTENT_TYPE');
36✔
183
        if (null === $contentType || '' === $contentType) {
36✔
184
            throw new UnsupportedMediaTypeHttpException('The "Content-Type" header must exist.');
8✔
185
        }
186

187
        $formatMatcher = new FormatMatcher($formats);
28✔
188
        $format = $formatMatcher->getFormat($contentType);
28✔
189
        if (null === $format) {
28✔
190
            $supportedMimeTypes = [];
4✔
191
            foreach ($formats as $mimeTypes) {
4✔
192
                foreach ($mimeTypes as $mimeType) {
4✔
193
                    $supportedMimeTypes[] = $mimeType;
4✔
194
                }
195
            }
196

197
            throw new UnsupportedMediaTypeHttpException(\sprintf('The content-type "%s" is not supported. Supported MIME types are "%s".', $contentType, implode('", "', $supportedMimeTypes)));
4✔
198
        }
199

200
        return $format;
24✔
201
    }
202
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc