Skip to content

add a specific "documentation_formats" that may differ from the API "… #1891

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions features/main/content_negotiation.feature
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ Feature: Content Negotiation support
Then the response status code should be 406
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"

Scenario: If the request format is HTML, the error should be in HTML
Copy link
Member

@dunglas dunglas Jun 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you keep this test (and duplicate it for XML if appropriate)? It checks that the documentation for any URL feature works (for instance when you open http://localhost/books/1, Swagger UI shows up and automatically runs the AJAX request).

When I add "Accept" header equal to "text/html"
Scenario: If the request format is XML, the error should be in XML
When I add "Accept" header equal to "application/xml"
And I send a "GET" request to "/dummies/666"
Then the response status code should be 404
And the header "Content-Type" should be equal to "text/html; charset=utf-8"
And the header "Content-Type" should be equal to "application/xml; charset=utf-8"
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ public function load(array $configs, ContainerBuilder $container)
$config = $this->processConfiguration($configuration, $configs);
$formats = $this->getFormats($config['formats']);
$errorFormats = $this->getFormats($config['error_formats']);
$this->handleConfig($container, $config, $formats, $errorFormats);
$documentationFormats = $this->getFormats($config['documentation_formats']);
$this->handleConfig($container, $config, $formats, $errorFormats, $documentationFormats);

$loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('api.xml');
Expand Down Expand Up @@ -153,8 +154,9 @@ public function load(array $configs, ContainerBuilder $container)
* @param array $config
* @param array $formats
* @param array $errorFormats
* @param array $documentationFormats
*/
private function handleConfig(ContainerBuilder $container, array $config, array $formats, array $errorFormats)
private function handleConfig(ContainerBuilder $container, array $config, array $formats, array $errorFormats, array $documentationFormats)
{
$container->setParameter('api_platform.enable_entrypoint', $config['enable_entrypoint']);
$container->setParameter('api_platform.enable_docs', $config['enable_docs']);
Expand All @@ -164,6 +166,7 @@ private function handleConfig(ContainerBuilder $container, array $config, array
$container->setParameter('api_platform.exception_to_status', $config['exception_to_status']);
$container->setParameter('api_platform.formats', $formats);
$container->setParameter('api_platform.error_formats', $errorFormats);
$container->setParameter('api_platform.documentation_formats', $documentationFormats);
$container->setParameter('api_platform.allow_plain_identifiers', $config['allow_plain_identifiers']);
$container->setParameter('api_platform.eager_loading.enabled', $config['eager_loading']['enabled']);
$container->setParameter('api_platform.eager_loading.max_joins', $config['eager_loading']['max_joins']);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,9 @@ public function getConfigTreeBuilder()

$this->addFormatSection($rootNode, 'formats', [
'jsonld' => ['mime_types' => ['application/ld+json']],
]);
$this->addFormatSection($rootNode, 'documentation_formats', [
'jsonld' => ['mime_types' => ['application/ld+json']], // Hydra documentation, admin and PWA generator
'json' => ['mime_types' => ['application/json']], // Swagger support
'html' => ['mime_types' => ['text/html']], // Swagger UI support
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTML must be defined also in addFormatSection section to have the "documentation for any URL" feature working.

]);
Expand Down
3 changes: 2 additions & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/api.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@
<service id="api_platform.listener.request.add_format" class="ApiPlatform\Core\EventListener\AddFormatListener">
<argument type="service" id="api_platform.negotiator" />
<argument>%api_platform.formats%</argument>
<argument>%api_platform.documentation_formats%</argument>

<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="7" />
</service>
Expand Down Expand Up @@ -205,7 +206,7 @@
<argument>%api_platform.title%</argument>
<argument>%api_platform.description%</argument>
<argument>%api_platform.version%</argument>
<argument>%api_platform.formats%</argument>
<argument>%api_platform.documentation_formats%</argument>
</service>

<service id="api_platform.action.exception" class="ApiPlatform\Core\Action\ExceptionAction" public="true">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<route id="api_doc" path="/docs.{_format}">
<default key="_controller">api_platform.action.documentation</default>
<default key="_api_respond">1</default>
<default key="_api_endpoint_type">documentation</default>
<default key="_format" />
</route>

Expand Down
2 changes: 1 addition & 1 deletion src/Bridge/Symfony/Bundle/Resources/config/swagger.xml
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
<argument>%api_platform.title%</argument>
<argument>%api_platform.description%</argument>
<argument>%api_platform.version%</argument>
<argument>%api_platform.formats%</argument>
<argument>%api_platform.documentation_formats%</argument>
<argument>%api_platform.oauth.enabled%</argument>
<argument>%api_platform.oauth.clientId%</argument>
<argument>%api_platform.oauth.clientSecret%</argument>
Expand Down
34 changes: 26 additions & 8 deletions src/EventListener/AddFormatListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ final class AddFormatListener
{
private $negotiator;
private $formats;
private $documentationFormats;
private $mimeTypes;

public function __construct(Negotiator $negotiator, array $formats)
public function __construct(Negotiator $negotiator, array $formats, array $documentationFormats = [])
{
$this->negotiator = $negotiator;
$this->formats = $formats;
$this->documentationFormats = $documentationFormats;
}

/**
Expand All @@ -51,13 +53,15 @@ public function onKernelRequest(GetResponseEvent $event)
return;
}

$this->populateMimeTypes();
$this->addRequestFormats($request, $this->formats);
$requestAcceptedFormats = $this->getRequestAcceptedFormats($request);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this private function can be replaced by 'documentation' === $request->attributes->get('_api_endpoint_type') ? $this->documentationFormats : $this->formats. It removes 2 function calls in the hot path.


$this->populateMimeTypes($requestAcceptedFormats);
$this->addRequestFormats($request, $requestAcceptedFormats);

// Empty strings must be converted to null because the Symfony router doesn't support parameter typing before 3.2 (_format)
if (null === $routeFormat = $request->attributes->get('_format') ?: null) {
$mimeTypes = array_keys($this->mimeTypes);
} elseif (!isset($this->formats[$routeFormat])) {
} elseif (!isset($requestAcceptedFormats[$routeFormat])) {
throw new NotFoundHttpException(sprintf('Format "%s" is not supported', $routeFormat));
} else {
$mimeTypes = Request::getMimeTypes($routeFormat);
Expand All @@ -84,11 +88,11 @@ public function onKernelRequest(GetResponseEvent $event)
return;
}

throw $this->getNotAcceptableHttpException($mimeType);
throw $this->getNotAcceptableHttpException($mimeType ?? $requestFormat);
Copy link
Member

@dunglas dunglas Jun 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$requestAcceptedFormats right? This should be covered by a test (because the error isn't caught now)

}

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

return;
Expand All @@ -110,15 +114,17 @@ private function addRequestFormats(Request $request, array $formats)

/**
* Populates the $mimeTypes property.
*
* @param array $requestAcceptedFormats
*/
private function populateMimeTypes()
private function populateMimeTypes(array $requestAcceptedFormats)
{
if (null !== $this->mimeTypes) {
return;
}

$this->mimeTypes = [];
foreach ($this->formats as $format => $mimeTypes) {
foreach ($requestAcceptedFormats as $format => $mimeTypes) {
foreach ($mimeTypes as $mimeType) {
$this->mimeTypes[$mimeType] = $format;
}
Expand All @@ -145,4 +151,16 @@ private function getNotAcceptableHttpException(string $accept, array $mimeTypes
implode('", "', $mimeTypes)
));
}

/**
* Retrieves the list of accepted format in the request depending on some parameters.
*/
private function getRequestAcceptedFormats(Request $request): array
{
if (!$request->attributes->has('_api_endpoint_type')) {
Copy link
Member

@dunglas dunglas Jun 4, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can call get directly and store the result in a variable to prevent an extra function call. (get will return null if the attribute is not defined)

Just drop this function, see below.

return $this->formats;
}

return 'documentation' === $request->attributes->get('_api_endpoint_type') ? $this->documentationFormats : $this->formats;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,7 @@ private function getPartialContainerBuilderProphecy($test = false)
'api_platform.collection.pagination.partial_parameter_name' => 'partial',
'api_platform.description' => 'description',
'api_platform.error_formats' => ['jsonproblem' => ['application/problem+json'], 'jsonld' => ['application/ld+json']],
'api_platform.documentation_formats' => ['jsonld' => ['application/ld+json'], 'json' => ['application/json'], 'html' => ['text/html']],
'api_platform.formats' => ['jsonld' => ['application/ld+json'], 'jsonhal' => ['application/hal+json']],
'api_platform.exception_to_status' => [ExceptionInterface::class => Response::HTTP_BAD_REQUEST, InvalidArgumentException::class => Response::HTTP_BAD_REQUEST],
'api_platform.title' => 'title',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ public function testDefaultConfig()
'version' => '1.0.0',
'formats' => [
'jsonld' => ['mime_types' => ['application/ld+json']],
],
'documentation_formats' => [
'jsonld' => ['mime_types' => ['application/ld+json']],
'json' => ['mime_types' => ['application/json']],
'html' => ['mime_types' => ['text/html']],
],
Expand Down
55 changes: 45 additions & 10 deletions tests/EventListener/AddFormatListenerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function testNoResourceClass()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['notexist' => 'application/vnd.notexist']);
$listener = new AddFormatListener(new Negotiator(), ['notexist' => 'application/vnd.notexist'], []);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As the new array argument has a default value we don't really need to change this right?

$listener->onKernelRequest($event);

$this->assertNull($request->getFormat('application/vnd.notexist'));
Expand All @@ -47,7 +47,7 @@ public function testSupportedRequestFormat()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']]);
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], []);
$listener->onKernelRequest($event);

$this->assertSame('xml', $request->getRequestFormat());
Expand All @@ -63,7 +63,7 @@ public function testRespondFlag()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']]);
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], []);
$listener->onKernelRequest($event);

$this->assertSame('xml', $request->getRequestFormat());
Expand All @@ -83,7 +83,7 @@ public function testUnsupportedRequestFormat()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']], []);
$listener->onKernelRequest($event);

$this->assertSame('json', $request->getRequestFormat());
Expand All @@ -98,7 +98,7 @@ public function testSupportedAcceptHeader()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['binary' => ['application/octet-stream'], 'json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['binary' => ['application/octet-stream'], 'json' => ['application/json']], []);
$listener->onKernelRequest($event);

$this->assertSame('json', $request->getRequestFormat());
Expand All @@ -113,7 +113,7 @@ public function testAcceptAllHeader()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['binary' => ['application/octet-stream'], 'json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['binary' => ['application/octet-stream'], 'json' => ['application/json']], []);
$listener->onKernelRequest($event);

$this->assertSame('binary', $request->getRequestFormat());
Expand All @@ -133,7 +133,7 @@ public function testUnsupportedAcceptHeader()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['binary' => ['application/octet-stream'], 'json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['binary' => ['application/octet-stream'], 'json' => ['application/json']], []);
$listener->onKernelRequest($event);
}

Expand All @@ -150,7 +150,7 @@ public function testInvalidAcceptHeader()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']], []);
$listener->onKernelRequest($event);
}

Expand All @@ -164,7 +164,7 @@ public function testAcceptHeaderTakePrecedenceOverRequestFormat()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['xml' => ['application/xml'], 'json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['xml' => ['application/xml'], 'json' => ['application/json']], []);
$listener->onKernelRequest($event);

$this->assertSame('json', $request->getRequestFormat());
Expand All @@ -182,7 +182,42 @@ public function testInvalidRouteFormat()
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']]);
$listener = new AddFormatListener(new Negotiator(), ['json' => ['application/json']], []);
$listener->onKernelRequest($event);
}

public function testSupportedDocumentationAcceptHeader()
{
$request = new Request([], [], ['_api_respond' => '1', '_api_endpoint_type' => 'documentation']);
$request->setRequestFormat('json');

$eventProphecy = $this->prophesize(GetResponseEvent::class);
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], ['json' => ['application/json']]);
$listener->onKernelRequest($event);

$this->assertSame('json', $request->getRequestFormat());
$this->assertSame('application/json', $request->getMimeType($request->getRequestFormat()));
}

/**
* @expectedException \Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException
* @expectedExceptionMessageRegExp /^Requested format "(jsonld|application\/ld\+json)" is not supported. Supported MIME types are "application\/json".$/
*/
public function testUnsupportedDocumentationRequestFormat()
{
$request = new Request([], [], ['_api_respond' => '1', '_api_endpoint_type' => 'documentation']);
$request->setRequestFormat('jsonld');

$eventProphecy = $this->prophesize(GetResponseEvent::class);
$eventProphecy->getRequest()->willReturn($request)->shouldBeCalled();
$event = $eventProphecy->reveal();

$listener = new AddFormatListener(new Negotiator(), ['xml' => ['text/xml']], ['json' => ['application/json']]);
$listener->onKernelRequest($event);

$this->assertSame('json', $request->getRequestFormat());
}
}
8 changes: 8 additions & 0 deletions tests/Fixtures/app/config/config_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,18 @@ api_platform:
xml: ['application/xml', 'text/xml']
json: ['application/json']
html: ['text/html']
documentation_formats:
jsonld: ['application/ld+json']
jsonhal: ['application/hal+json']
jsonapi: ['application/vnd.api+json']
xml: ['application/xml', 'text/xml']
json: ['application/json']
html: ['text/html']
error_formats:
jsonproblem: ['application/problem+json']
jsonld: ['application/ld+json']
jsonapi: ['application/vnd.api+json']
xml: ['application/xml', 'text/xml']
graphql: true
name_converter: 'app.name_converter'
enable_fos_user: true
Expand Down