1 | <?php |
||
2 | |||
3 | /* |
||
4 | * This file is part of the API Platform project. |
||
5 | * |
||
6 | * (c) Kévin Dunglas <[email protected]> |
||
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\Core\EventListener; |
||
15 | |||
16 | use ApiPlatform\Core\Api\FormatMatcher; |
||
17 | use ApiPlatform\Core\Api\FormatsProviderInterface; |
||
18 | use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; |
||
19 | use ApiPlatform\Core\Util\RequestAttributesExtractor; |
||
20 | use Negotiation\Negotiator; |
||
21 | use Symfony\Component\HttpFoundation\Request; |
||
22 | use Symfony\Component\HttpKernel\Event\GetResponseEvent; |
||
23 | use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; |
||
24 | use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; |
||
25 | |||
26 | /** |
||
27 | * Chooses the format to use according to the Accept header and supported formats. |
||
28 | * |
||
29 | * @author Kévin Dunglas <[email protected]> |
||
30 | */ |
||
31 | final class AddFormatListener |
||
32 | { |
||
33 | private $negotiator; |
||
34 | private $resourceMetadataFactory; |
||
35 | private $formats = []; |
||
36 | private $formatsProvider; |
||
37 | private $formatMatcher; |
||
38 | |||
39 | /** |
||
40 | * @param ResourceMetadataFactoryInterface|FormatsProviderInterface|array $resourceMetadataFactory |
||
41 | */ |
||
42 | public function __construct(Negotiator $negotiator, $resourceMetadataFactory, array $formats = []) |
||
43 | { |
||
44 | $this->negotiator = $negotiator; |
||
45 | $this->resourceMetadataFactory = $resourceMetadataFactory instanceof ResourceMetadataFactoryInterface ? $resourceMetadataFactory : null; |
||
46 | $this->formats = $formats; |
||
47 | |||
48 | if (!$resourceMetadataFactory instanceof ResourceMetadataFactoryInterface) { |
||
49 | @trigger_error(sprintf('Passing an array or an instance of "%s" as 2nd parameter of the constructor of "%s" is deprecated since API Platform 2.5, pass an instance of "%s" instead', FormatsProviderInterface::class, __CLASS__, ResourceMetadataFactoryInterface::class), E_USER_DEPRECATED); |
||
50 | } |
||
51 | |||
52 | if (\is_array($resourceMetadataFactory)) { |
||
53 | $this->formats = $resourceMetadataFactory; |
||
54 | } elseif ($resourceMetadataFactory instanceof FormatsProviderInterface) { |
||
55 | $this->formatsProvider = $resourceMetadataFactory; |
||
56 | } |
||
57 | } |
||
58 | |||
59 | /** |
||
60 | * Sets the applicable format to the HttpFoundation Request. |
||
61 | * |
||
62 | * @throws NotFoundHttpException |
||
63 | * @throws NotAcceptableHttpException |
||
64 | */ |
||
65 | public function onKernelRequest(GetResponseEvent $event): void |
||
66 | { |
||
67 | $request = $event->getRequest(); |
||
68 | if ( |
||
69 | !($request->attributes->has('_api_resource_class') |
||
70 | || $request->attributes->getBoolean('_api_respond', false) |
||
71 | || $request->attributes->getBoolean('_graphql', false)) |
||
72 | ) { |
||
73 | return; |
||
74 | } |
||
75 | |||
76 | $attributes = RequestAttributesExtractor::extractAttributes($request); |
||
77 | |||
78 | // BC check to be removed in 3.0 |
||
79 | if ($this->resourceMetadataFactory) { |
||
80 | if ($attributes) { |
||
0 ignored issues
–
show
|
|||
81 | // TODO: Subresource operation metadata aren't available by default, for now we have to fallback on default formats. |
||
82 | // TODO: A better approach would be to always populate the subresource operation array. |
||
83 | $formats = $this |
||
84 | ->resourceMetadataFactory |
||
85 | ->create($attributes['resource_class']) |
||
86 | ->getOperationAttribute($attributes, 'output_formats', $this->formats, true); |
||
87 | } else { |
||
88 | $formats = $this->formats; |
||
89 | } |
||
90 | } elseif ($this->formatsProvider instanceof FormatsProviderInterface) { |
||
91 | $formats = $this->formatsProvider->getFormatsFromAttributes($attributes); |
||
92 | } else { |
||
93 | $formats = $this->formats; |
||
94 | } |
||
95 | |||
96 | $this->addRequestFormats($request, $formats); |
||
97 | $this->formatMatcher = new FormatMatcher($formats); |
||
98 | |||
99 | // Empty strings must be converted to null because the Symfony router doesn't support parameter typing before 3.2 (_format) |
||
100 | if (null === $routeFormat = $request->attributes->get('_format') ?: null) { |
||
101 | $flattenedMimeTypes = $this->flattenMimeTypes($formats); |
||
102 | $mimeTypes = array_keys($flattenedMimeTypes); |
||
103 | } elseif (!isset($formats[$routeFormat])) { |
||
104 | throw new NotFoundHttpException(sprintf('Format "%s" is not supported', $routeFormat)); |
||
105 | } else { |
||
106 | $mimeTypes = Request::getMimeTypes($routeFormat); |
||
107 | $flattenedMimeTypes = $this->flattenMimeTypes([$routeFormat => $mimeTypes]); |
||
108 | } |
||
109 | |||
110 | // First, try to guess the format from the Accept header |
||
111 | /** @var string|null $accept */ |
||
112 | $accept = $request->headers->get('Accept'); |
||
113 | if (null !== $accept) { |
||
114 | if (null === $mediaType = $this->negotiator->getBest($accept, $mimeTypes)) { |
||
115 | throw $this->getNotAcceptableHttpException($accept, $flattenedMimeTypes); |
||
116 | } |
||
117 | |||
118 | $request->setRequestFormat($this->formatMatcher->getFormat($mediaType->getType())); |
||
119 | |||
120 | return; |
||
121 | } |
||
122 | |||
123 | // Then use the Symfony request format if available and applicable |
||
124 | $requestFormat = $request->getRequestFormat('') ?: null; |
||
125 | if (null !== $requestFormat) { |
||
126 | $mimeType = $request->getMimeType($requestFormat); |
||
127 | |||
128 | if (isset($flattenedMimeTypes[$mimeType])) { |
||
129 | return; |
||
130 | } |
||
131 | |||
132 | throw $this->getNotAcceptableHttpException($mimeType, $flattenedMimeTypes); |
||
133 | } |
||
134 | |||
135 | // Finally, if no Accept header nor Symfony request format is set, return the default format |
||
136 | foreach ($formats as $format => $mimeType) { |
||
137 | $request->setRequestFormat($format); |
||
138 | |||
139 | return; |
||
140 | } |
||
141 | } |
||
142 | |||
143 | /** |
||
144 | * Adds the supported formats to the request. |
||
145 | * |
||
146 | * This is necessary for {@see Request::getMimeType} and {@see Request::getMimeTypes} to work. |
||
147 | */ |
||
148 | private function addRequestFormats(Request $request, array $formats): void |
||
149 | { |
||
150 | foreach ($formats as $format => $mimeTypes) { |
||
151 | $request->setFormat($format, (array) $mimeTypes); |
||
152 | } |
||
153 | } |
||
154 | |||
155 | /** |
||
156 | * Retries the flattened list of MIME types. |
||
157 | */ |
||
158 | private function flattenMimeTypes(array $formats): array |
||
159 | { |
||
160 | $flattenedMimeTypes = []; |
||
161 | foreach ($formats as $format => $mimeTypes) { |
||
162 | foreach ($mimeTypes as $mimeType) { |
||
163 | $flattenedMimeTypes[$mimeType] = $format; |
||
164 | } |
||
165 | } |
||
166 | |||
167 | return $flattenedMimeTypes; |
||
168 | } |
||
169 | |||
170 | /** |
||
171 | * Retrieves an instance of NotAcceptableHttpException. |
||
172 | */ |
||
173 | private function getNotAcceptableHttpException(string $accept, array $mimeTypes): NotAcceptableHttpException |
||
174 | { |
||
175 | return new NotAcceptableHttpException(sprintf( |
||
176 | 'Requested format "%s" is not supported. Supported MIME types are "%s".', |
||
177 | $accept, |
||
178 | implode('", "', array_keys($mimeTypes)) |
||
179 | )); |
||
180 | } |
||
181 | } |
||
182 |
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.
Consider making the comparison explicit by using
empty(..)
or! empty(...)
instead.