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

api-platform / core / 9219481874

24 May 2024 06:10AM CUT coverage: 57.41% (+0.02%) from 57.392%
9219481874

push

github

web-flow
feat(openapi): allow optional request body content (#6374)

14 of 14 new or added lines in 1 file covered. (100.0%)

58 existing lines in 14 files now uncovered.

10026 of 17464 relevant lines covered (57.41%)

48.72 hits per line

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

68.13
/src/Symfony/EventListener/AddFormatListener.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\Error as ErrorOperation;
18
use ApiPlatform\Metadata\HttpOperation;
19
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20
use ApiPlatform\State\ProviderInterface;
21
use ApiPlatform\State\Util\OperationRequestInitiatorTrait;
22
use ApiPlatform\Symfony\Util\RequestAttributesExtractor;
23
use Negotiation\Exception\InvalidArgument;
24
use Negotiation\Negotiator;
25
use Symfony\Component\HttpFoundation\Request;
26
use Symfony\Component\HttpKernel\Event\RequestEvent;
27
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
28
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
29

30
/**
31
 * Chooses the format to use according to the Accept header and supported formats.
32
 *
33
 * @author Kévin Dunglas <dunglas@gmail.com>
34
 */
35
final class AddFormatListener
36
{
37
    use OperationRequestInitiatorTrait;
38

39
    private ?Negotiator $negotiator;
40
    private ?ProviderInterface $provider = null;
41

42
    /**
43
     * @param ProviderInterface|Negotiator $negotiator
44
     */
45
    public function __construct($negotiator, ?ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly array $formats = [], private readonly array $errorFormats = [], private readonly array $docsFormats = [], private readonly ?bool $eventsBackwardCompatibility = null) // @phpstan-ignore-line
46
    {
47
        if ($negotiator instanceof ProviderInterface) {
98✔
48
            $this->provider = $negotiator;
×
49
        } else {
50
            trigger_deprecation('api-platform/core', '3.3', 'Use a "%s" as first argument in "%s" instead of "%s".', ProviderInterface::class, self::class, Negotiator::class);
98✔
51
            $this->negotiator = $negotiator;
98✔
52
        }
53

54
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
98✔
55
    }
56

57
    /**
58
     * Sets the applicable format to the HttpFoundation Request.
59
     *
60
     * @throws NotFoundHttpException
61
     * @throws NotAcceptableHttpException
62
     */
63
    public function onKernelRequest(RequestEvent $event): void
64
    {
65
        $request = $event->getRequest();
98✔
66
        $operation = $this->initializeOperation($request);
98✔
67

68
        // TODO: legacy code
69
        if ($request->attributes->get('_api_exception_action')) {
98✔
UNCOV
70
            return;
1✔
71
        }
72

73
        $attributes = RequestAttributesExtractor::extractAttributes($request);
98✔
74
        if (!($attributes['respond'] ?? $request->attributes->getBoolean('_api_respond'))) {
98✔
75
            return;
8✔
76
        }
77

78
        if ($operation && $this->provider) {
94✔
79
            $this->provider->provide($operation, $request->attributes->get('_api_uri_variables') ?? [], [
×
80
                'request' => $request,
×
81
                'uri_variables' => $request->attributes->get('_api_uri_variables') ?? [],
×
82
                'resource_class' => $operation->getClass(),
×
83
            ]);
×
84

85
            return;
×
86
        }
87

88
        // TODO: the code below needs to be removed in 4.x
89
        if ($this->provider && !$operation) {
94✔
90
            return;
×
91
        }
92

93
        if ('api_platform.action.entrypoint' === $request->attributes->get('_controller')) {
94✔
UNCOV
94
            return;
7✔
95
        }
96

97
        if ('api_platform.symfony.main_controller' === $operation?->getController() || $request->attributes->get('_api_platform_disable_listeners')) {
87✔
UNCOV
98
            return;
32✔
99
        }
100

101
        if ($operation instanceof ErrorOperation) {
55✔
102
            return;
×
103
        }
104

105
        if (!(
106
            $request->attributes->has('_api_resource_class')
55✔
107
            || $request->attributes->getBoolean('_api_respond', false)
55✔
108
            || $request->attributes->getBoolean('_graphql', false)
55✔
109
        )) {
110
            return;
×
111
        }
112

113
        $formats = $operation?->getOutputFormats() ?? ('api_doc' === $request->attributes->get('_route') ? $this->docsFormats : $this->formats);
55✔
114

115
        $this->addRequestFormats($request, $formats);
55✔
116

117
        // Empty strings must be converted to null because the Symfony router doesn't support parameter typing before 3.2 (_format)
118
        if (null === $routeFormat = $request->attributes->get('_format') ?: null) {
55✔
119
            $flattenedMimeTypes = $this->flattenMimeTypes($formats);
48✔
120
            $mimeTypes = array_keys($flattenedMimeTypes);
48✔
121
        } elseif (!isset($formats[$routeFormat])) {
7✔
122
            if (!$request->attributes->get('data') instanceof \Exception) {
4✔
123
                throw new NotFoundHttpException(sprintf('Format "%s" is not supported', $routeFormat));
4✔
124
            }
125
            $this->setRequestErrorFormat($operation, $request);
×
126

127
            return;
×
128
        } else {
UNCOV
129
            $mimeTypes = Request::getMimeTypes($routeFormat);
3✔
UNCOV
130
            $flattenedMimeTypes = $this->flattenMimeTypes([$routeFormat => $mimeTypes]);
3✔
131
        }
132

133
        // First, try to guess the format from the Accept header
134
        /** @var string|null $accept */
135
        $accept = $request->headers->get('Accept');
51✔
136

137
        if (null !== $accept) {
51✔
138
            $mediaType = null;
35✔
139
            try {
140
                $mediaType = $this->negotiator->getBest($accept, $mimeTypes);
35✔
141
            } catch (InvalidArgument) {
4✔
142
                throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes);
4✔
143
            }
144

145
            if (null === $mediaType) {
31✔
146
                if (!$request->attributes->get('data') instanceof \Exception) {
12✔
147
                    throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes);
12✔
148
                }
149

150
                $this->setRequestErrorFormat($operation, $request);
×
151

152
                return;
×
153
            }
154
            $formatMatcher = new FormatMatcher($formats);
19✔
155
            $request->setRequestFormat($formatMatcher->getFormat($mediaType->getType()));
19✔
156

157
            return;
19✔
158
        }
159

160
        // Then use the Symfony request format if available and applicable
161
        $requestFormat = $request->getRequestFormat('') ?: null;
16✔
162
        if (null !== $requestFormat) {
16✔
163
            $mimeType = $request->getMimeType($requestFormat);
16✔
164

165
            if (isset($flattenedMimeTypes[$mimeType])) {
16✔
166
                return;
12✔
167
            }
168

169
            if ($request->attributes->get('data') instanceof \Exception) {
4✔
170
                $this->setRequestErrorFormat($operation, $request);
×
171

172
                return;
×
173
            }
174

175
            throw $this->getNotAcceptableHttpException($mimeType, $flattenedMimeTypes);
4✔
176
        }
177

178
        // Finally, if no Accept header nor Symfony request format is set, return the default format
179
        foreach ($formats as $format => $mimeType) {
×
180
            $request->setRequestFormat($format);
×
181

182
            return;
×
183
        }
184
    }
185

186
    /**
187
     * Adds the supported formats to the request.
188
     *
189
     * This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work.
190
     */
191
    private function addRequestFormats(Request $request, array $formats): void
192
    {
193
        foreach ($formats as $format => $mimeTypes) {
55✔
194
            $request->setFormat($format, (array) $mimeTypes);
55✔
195
        }
196
    }
197

198
    /**
199
     * Retries the flattened list of MIME types.
200
     */
201
    private function flattenMimeTypes(array $formats): array
202
    {
203
        $flattenedMimeTypes = [];
51✔
204
        foreach ($formats as $format => $mimeTypes) {
51✔
205
            foreach ($mimeTypes as $mimeType) {
51✔
206
                $flattenedMimeTypes[$mimeType] = $format;
51✔
207
            }
208
        }
209

210
        return $flattenedMimeTypes;
51✔
211
    }
212

213
    /**
214
     * Retrieves an instance of NotAcceptableHttpException.
215
     */
216
    private function getNotAcceptableHttpException(string $accept, array $mimeTypes): NotAcceptableHttpException
217
    {
218
        return new NotAcceptableHttpException(sprintf(
20✔
219
            'Requested format "%s" is not supported. Supported MIME types are "%s".',
20✔
220
            $accept,
20✔
221
            implode('", "', array_keys($mimeTypes))
20✔
222
        ));
20✔
223
    }
224

225
    public function setRequestErrorFormat(?HttpOperation $operation, Request $request): void
226
    {
227
        $errorResourceFormats = array_merge($operation?->getOutputFormats() ?? [], $operation?->getFormats() ?? [], $this->errorFormats);
×
228

229
        $flattened = $this->flattenMimeTypes($errorResourceFormats);
×
230
        if ($flattened[$accept = $request->headers->get('Accept')] ?? false) {
×
231
            $request->setRequestFormat($flattened[$accept]);
×
232

233
            return;
×
234
        }
235

236
        if (isset($errorResourceFormats['jsonproblem'])) {
×
237
            $request->setRequestFormat('jsonproblem');
×
238
            $request->setFormat('jsonproblem', $errorResourceFormats['jsonproblem']);
×
239

240
            return;
×
241
        }
242

243
        $request->setRequestFormat(array_key_first($errorResourceFormats));
×
244
    }
245
}
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