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

api-platform / core / 6978770879

24 Nov 2023 09:02AM UTC coverage: 37.284% (-0.1%) from 37.409%
6978770879

push

github

soyuka
Merge 3.2

79 of 149 new or added lines in 21 files covered. (53.02%)

16 existing lines in 8 files now uncovered.

10287 of 27591 relevant lines covered (37.28%)

20.53 hits per line

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

76.06
/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\Util\OperationRequestInitiatorTrait;
21
use Negotiation\Exception\InvalidArgument;
22
use Negotiation\Negotiator;
23
use Symfony\Component\HttpFoundation\Request;
24
use Symfony\Component\HttpKernel\Event\RequestEvent;
25
use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException;
26
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
27

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

37
    public function __construct(private readonly Negotiator $negotiator, ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory = null, private readonly array $formats = [], private readonly array $errorFormats = [], private readonly array $docsFormats = [], private readonly bool $eventsBackwardCompatibility = true)
38
    {
39
        $this->resourceMetadataCollectionFactory = $resourceMetadataCollectionFactory;
120✔
40
    }
41

42
    /**
43
     * Sets the applicable format to the HttpFoundation Request.
44
     *
45
     * @throws NotFoundHttpException
46
     * @throws NotAcceptableHttpException
47
     */
48
    public function onKernelRequest(RequestEvent $event): void
49
    {
50
        $request = $event->getRequest();
120✔
51
        $operation = $this->initializeOperation($request);
120✔
52

53
        if ('api_platform.symfony.main_controller' === $operation?->getController() || ($this->eventsBackwardCompatibility && 'api_platform.action.entrypoint' === $request->attributes->get('_controller')) || $request->attributes->get('_api_platform_disable_listeners')) {
120✔
54
            return;
37✔
55
        }
56

57
        if ($operation instanceof ErrorOperation) {
89✔
UNCOV
58
            return;
×
59
        }
60

61
        if (!($request->attributes->has('_api_resource_class')
89✔
62
            || $request->attributes->getBoolean('_api_respond', false)
89✔
63
            || $request->attributes->getBoolean('_graphql', false)
89✔
64
        )) {
65
            return;
19✔
66
        }
67

68
        $formats = $operation?->getOutputFormats() ?? ('api_doc' === $request->attributes->get('_route') ? $this->docsFormats : $this->formats);
77✔
69

70
        $this->addRequestFormats($request, $formats);
77✔
71

72
        // Empty strings must be converted to null because the Symfony router doesn't support parameter typing before 3.2 (_format)
73
        if (null === $routeFormat = $request->attributes->get('_format') ?: null) {
77✔
74
            $flattenedMimeTypes = $this->flattenMimeTypes($formats);
65✔
75
            $mimeTypes = array_keys($flattenedMimeTypes);
65✔
76
        } elseif (!isset($formats[$routeFormat])) {
12✔
77
            if (!$request->attributes->get('data') instanceof \Exception) {
3✔
78
                throw new NotFoundHttpException(sprintf('Format "%s" is not supported', $routeFormat));
3✔
79
            }
80
            $this->setRequestErrorFormat($operation, $request);
×
81

82
            return;
×
83
        } else {
84
            $mimeTypes = Request::getMimeTypes($routeFormat);
9✔
85
            $flattenedMimeTypes = $this->flattenMimeTypes([$routeFormat => $mimeTypes]);
9✔
86
        }
87

88
        // First, try to guess the format from the Accept header
89
        /** @var string|null $accept */
90
        $accept = $request->headers->get('Accept');
74✔
91

92
        if (null !== $accept) {
74✔
93
            $mediaType = null;
58✔
94
            try {
95
                $mediaType = $this->negotiator->getBest($accept, $mimeTypes);
58✔
96
            } catch (InvalidArgument) {
3✔
97
                throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes);
3✔
98
            }
99

100
            if (null === $mediaType) {
55✔
101
                if (!$request->attributes->get('data') instanceof \Exception) {
9✔
102
                    throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes);
9✔
103
                }
104

105
                $this->setRequestErrorFormat($operation, $request);
×
106

107
                return;
×
108
            }
109
            $formatMatcher = new FormatMatcher($formats);
46✔
110
            $request->setRequestFormat($formatMatcher->getFormat($mediaType->getType()));
46✔
111

112
            return;
46✔
113
        }
114

115
        // Then use the Symfony request format if available and applicable
116
        $requestFormat = $request->getRequestFormat('') ?: null;
16✔
117
        if (null !== $requestFormat) {
16✔
118
            $mimeType = $request->getMimeType($requestFormat);
12✔
119

120
            if (isset($flattenedMimeTypes[$mimeType])) {
12✔
121
                return;
9✔
122
            }
123

124
            if ($request->attributes->get('data') instanceof \Exception) {
3✔
125
                $this->setRequestErrorFormat($operation, $request);
×
126

127
                return;
×
128
            }
129

130
            throw $this->getNotAcceptableHttpException($mimeType, $flattenedMimeTypes);
3✔
131
        }
132

133
        // Finally, if no Accept header nor Symfony request format is set, return the default format
134
        foreach ($formats as $format => $mimeType) {
4✔
135
            $request->setRequestFormat($format);
4✔
136

137
            return;
4✔
138
        }
139
    }
140

141
    /**
142
     * Adds the supported formats to the request.
143
     *
144
     * This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work.
145
     */
146
    private function addRequestFormats(Request $request, array $formats): void
147
    {
148
        foreach ($formats as $format => $mimeTypes) {
77✔
149
            $request->setFormat($format, (array) $mimeTypes);
77✔
150
        }
151
    }
152

153
    /**
154
     * Retries the flattened list of MIME types.
155
     */
156
    private function flattenMimeTypes(array $formats): array
157
    {
158
        $flattenedMimeTypes = [];
74✔
159
        foreach ($formats as $format => $mimeTypes) {
74✔
160
            foreach ($mimeTypes as $mimeType) {
74✔
161
                $flattenedMimeTypes[$mimeType] = $format;
74✔
162
            }
163
        }
164

165
        return $flattenedMimeTypes;
74✔
166
    }
167

168
    /**
169
     * Retrieves an instance of NotAcceptableHttpException.
170
     */
171
    private function getNotAcceptableHttpException(string $accept, array $mimeTypes): NotAcceptableHttpException
172
    {
173
        return new NotAcceptableHttpException(sprintf(
15✔
174
            'Requested format "%s" is not supported. Supported MIME types are "%s".',
15✔
175
            $accept,
15✔
176
            implode('", "', array_keys($mimeTypes))
15✔
177
        ));
15✔
178
    }
179

180
    public function setRequestErrorFormat(?HttpOperation $operation, Request $request): void
181
    {
182
        $errorResourceFormats = array_merge($operation?->getOutputFormats() ?? [], $operation?->getFormats() ?? [], $this->errorFormats);
×
183

184
        $flattened = $this->flattenMimeTypes($errorResourceFormats);
×
185
        if ($flattened[$accept = $request->headers->get('Accept')] ?? false) {
×
186
            $request->setRequestFormat($flattened[$accept]);
×
187

188
            return;
×
189
        }
190

191
        if (isset($errorResourceFormats['jsonproblem'])) {
×
192
            $request->setRequestFormat('jsonproblem');
×
193
            $request->setFormat('jsonproblem', $errorResourceFormats['jsonproblem']);
×
194

195
            return;
×
196
        }
197

198
        $request->setRequestFormat(array_key_first($errorResourceFormats));
×
199
    }
200
}
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