Skip to content

Commit dad282d

Browse files
Unknownantograssiot
authored andcommitted
Allow to specify formats per resources/operations
1 parent d2b657e commit dad282d

File tree

3 files changed

+157
-2
lines changed

3 files changed

+157
-2
lines changed

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
<service id="api_platform.listener.request.add_format" class="ApiPlatform\Core\EventListener\AddFormatListener">
130130
<argument type="service" id="api_platform.negotiator" />
131131
<argument>%api_platform.formats%</argument>
132+
<argument type="service" id="api_platform.metadata.resource.metadata_factory"></argument>
132133

133134
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="7" />
134135
</service>

src/EventListener/AddFormatListener.php

Lines changed: 68 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,12 @@
1313

1414
namespace ApiPlatform\Core\EventListener;
1515

16+
use ApiPlatform\Core\Exception\InvalidArgumentException;
17+
use ApiPlatform\Core\Metadata\Resource\Factory\AnnotationResourceMetadataFactory;
18+
use ApiPlatform\Core\Metadata\Resource\Factory\OperationResourceMetadataFactory;
19+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
20+
use ApiPlatform\Core\Serializer\SerializerContextBuilderInterface;
21+
use ApiPlatform\Core\Util\RequestAttributesExtractor;
1622
use Negotiation\Negotiator;
1723
use Symfony\Component\HttpFoundation\Request;
1824
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
@@ -29,11 +35,13 @@ final class AddFormatListener
2935
private $negotiator;
3036
private $formats;
3137
private $mimeTypes;
38+
private $resourceMetadataFactory;
3239

33-
public function __construct(Negotiator $negotiator, array $formats)
40+
public function __construct(Negotiator $negotiator, array $formats, ResourceMetadataFactoryInterface $resourceMetadataFactory = null)
3441
{
3542
$this->negotiator = $negotiator;
3643
$this->formats = $formats;
44+
$this->resourceMetadataFactory = $resourceMetadataFactory;
3745
}
3846

3947
/**
@@ -47,10 +55,17 @@ public function __construct(Negotiator $negotiator, array $formats)
4755
public function onKernelRequest(GetResponseEvent $event)
4856
{
4957
$request = $event->getRequest();
50-
if (!$request->attributes->has('_api_resource_class') && !$request->attributes->has('_api_respond') && !$request->attributes->has('_graphql')) {
58+
if (!($resourceClass = $request->attributes->get('_api_resource_class')) && !$request->attributes->has('_api_respond') && !$request->attributes->has('_graphql')) {
5159
return;
5260
}
5361

62+
if (null !== $this->resourceMetadataFactory && null !== $resourceClass) {
63+
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
64+
$attributes = RequestAttributesExtractor::extractAttributes($request);
65+
$formats = $resourceMetadata->getOperationAttribute($attributes, 'formats', [], true);
66+
$this->populateFormats($formats);
67+
}
68+
5469
$this->populateMimeTypes();
5570
$this->addRequestFormats($request, $this->formats);
5671

@@ -125,6 +140,57 @@ private function populateMimeTypes()
125140
}
126141
}
127142

143+
/**
144+
* Populates the $format property.
145+
*/
146+
private function populateFormats(array $annotationFormats)
147+
{
148+
if (!$annotationFormats) {
149+
return;
150+
}
151+
152+
$annotationFormats = $this->normalizeAnnotationFormats($annotationFormats);
153+
$resourceFormats = [];
154+
foreach ($annotationFormats as $format => $value) {
155+
if (null !== $value) {
156+
$resourceFormats[$format] = $value;
157+
continue;
158+
} elseif (array_key_exists($format, $this->formats)) {
159+
$resourceFormats[$format] = $this->formats[$format];
160+
continue;
161+
}
162+
throw new InvalidArgumentException(sprintf("You either need to add the format '%s' to your project configuration or declare a mime type for it in your annotation.", $format));
163+
}
164+
165+
$this->formats = $resourceFormats;
166+
}
167+
168+
/**
169+
* Normalizes the format from config to the one accepted by Symfony HttpFoundation.
170+
*/
171+
private function normalizeAnnotationFormats(array $annotationFormats): array
172+
{
173+
$formats = [];
174+
foreach ($annotationFormats as $format => $value) {
175+
if (!is_numeric($format) && !\is_array($value)) {
176+
throw new InvalidArgumentException(sprintf("Mime type for format '%s' must be declared as an array.", $format));
177+
}
178+
if (is_numeric($format)) {
179+
$formats[$value] = null;
180+
continue;
181+
}
182+
if (isset($value['mime_types'])) {
183+
foreach ($value['mime_types'] as $mimeType) {
184+
$formats[$format][] = $mimeType;
185+
}
186+
continue;
187+
}
188+
$formats[$format] = $value;
189+
}
190+
191+
return $formats;
192+
}
193+
128194
/**
129195
* Retrieves an instance of NotAcceptableHttpException.
130196
*

tests/EventListener/AddFormatListenerTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Core\Tests\EventListener;
1515

1616
use ApiPlatform\Core\EventListener\AddFormatListener;
17+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18+
use ApiPlatform\Core\Metadata\Resource\ResourceMetadata;
1719
use Negotiation\Negotiator;
1820
use PHPUnit\Framework\TestCase;
1921
use Symfony\Component\HttpFoundation\Request;
@@ -185,4 +187,90 @@ public function testInvalidRouteFormat()
185187
$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']]);
186188
$listener->onKernelRequest($event);
187189
}
190+
191+
public function testSupportedRequestFormatAndResourceClassWithoutSpecifiedFormatsInAnnotation()
192+
{
193+
$request = new Request([], [], ['_api_resource_class' => 'Foo']);
194+
$request->setRequestFormat('xml');
195+
196+
$eventProphecy = $this->prophesize(GetResponseEvent::class);
197+
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
198+
$event = $eventProphecy->reveal();
199+
200+
$dummyMetadata = new ResourceMetadata();
201+
202+
$resourceMetaDataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
203+
$resourceMetaDataFactoryProphecy->create('Foo')->willReturn($dummyMetadata)->shouldbeCalled();
204+
205+
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], $resourceMetaDataFactoryProphecy->reveal());
206+
$listener->onKernelRequest($event);
207+
208+
$this->assertSame('xml', $request->getRequestFormat());
209+
$this->assertSame('text/xml', $request->getMimeType($request->getRequestFormat()));
210+
}
211+
212+
public function testSupportedRequestFormatAndResourceClassWithSpecifiedFormatsInAnnotation()
213+
{
214+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']);
215+
$request->setRequestFormat('pdf');
216+
217+
$eventProphecy = $this->prophesize(GetResponseEvent::class);
218+
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
219+
$event = $eventProphecy->reveal();
220+
221+
$dummyMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => ['pdf' => ['application/pdf']]]);
222+
223+
$resourceMetaDataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
224+
$resourceMetaDataFactoryProphecy->create('Foo')->willReturn($dummyMetadata)->shouldbeCalled();
225+
226+
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml'], 'jsonld'], $resourceMetaDataFactoryProphecy->reveal());
227+
$listener->onKernelRequest($event);
228+
229+
$this->assertSame('pdf', $request->getRequestFormat());
230+
$this->assertSame('application/pdf', $request->getMimeType($request->getRequestFormat()));
231+
}
232+
233+
/**
234+
* @expectedException \Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException
235+
* @expectedExceptionMessage Requested format "text/xml" is not supported. Supported MIME types are "application/json".
236+
*/
237+
public function testResourceClassWithSpecifiedFormatsInAnnotationOverridesDefault()
238+
{
239+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']);
240+
$request->setRequestFormat('xml');
241+
242+
$eventProphecy = $this->prophesize(GetResponseEvent::class);
243+
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
244+
$event = $eventProphecy->reveal();
245+
246+
$dummyMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => ['json' => ['application/json']]]);
247+
248+
$resourceMetaDataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
249+
$resourceMetaDataFactoryProphecy->create('Foo')->willReturn($dummyMetadata)->shouldbeCalled();
250+
251+
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], $resourceMetaDataFactoryProphecy->reveal());
252+
$listener->onKernelRequest($event);
253+
}
254+
255+
/**
256+
* @expectedException \ApiPlatform\Core\Exception\InvalidArgumentException
257+
* @expectedExceptionMessage You either need to add the format 'json' to your project configuration or declare a mime type for it in your annotation.
258+
*/
259+
public function testResourceClassWithShortFormatsInAnnotationThatIsNotInConfig()
260+
{
261+
$request = new Request([], [], ['_api_resource_class' => 'Foo', '_api_collection_operation_name' => 'get']);
262+
$request->setRequestFormat('xml');
263+
264+
$eventProphecy = $this->prophesize(GetResponseEvent::class);
265+
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
266+
$event = $eventProphecy->reveal();
267+
268+
$dummyMetadata = new ResourceMetadata(null, null, null, null, null, ['formats' => ['json']]);
269+
270+
$resourceMetaDataFactoryProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class);
271+
$resourceMetaDataFactoryProphecy->create('Foo')->willReturn($dummyMetadata)->shouldbeCalled();
272+
273+
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], $resourceMetaDataFactoryProphecy->reveal());
274+
$listener->onKernelRequest($event);
275+
}
188276
}

0 commit comments

Comments
 (0)