Skip to content

Commit 2201faa

Browse files
committed
Allow to specify formats per resources/operations
Add unit tests Update service injection with deprecation error and exception Fix missing use of FormatsProvider into DocumentAction Only support the shortest mime types declaration Update service injection in documentation to trigger deprecation warning Avoid another way to badly declare format overrding. update phpdoc
1 parent c304b11 commit 2201faa

File tree

19 files changed

+660
-36
lines changed

19 files changed

+660
-36
lines changed

features/main/content_negotiation.feature

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,36 @@ Feature: Content Negotiation support
115115
And I send a "GET" request to "/dummies/666"
116116
Then the response status code should be 404
117117
And the header "Content-Type" should be equal to "text/html; charset=utf-8"
118+
119+
Scenario: Retrieve a collection in JSON should not be possible if the format has been removed at resource level
120+
When I add "Accept" header equal to "application/json"
121+
And I send a "GET" request to "/dummy_custom_formats"
122+
Then the response status code should be 406
123+
And the header "Content-Type" should be equal to "application/problem+json; charset=utf-8"
124+
125+
Scenario: Post an CSV body allowed on a single resource
126+
When I add "Accept" header equal to "application/xml"
127+
And I add "Content-Type" header equal to "text/csv"
128+
And I send a "POST" request to "/dummy_custom_formats" with body:
129+
"""
130+
name
131+
Kevin
132+
"""
133+
Then the response status code should be 201
134+
And the header "Content-Type" should be equal to "application/xml; charset=utf-8"
135+
And the response should be equal to
136+
"""
137+
<?xml version="1.0"?>
138+
<response><id>1</id><name>Kevin</name></response>
139+
"""
140+
141+
Scenario: Retrieve a collection in CSV should be possible if the format is at resource level
142+
When I add "Accept" header equal to "text/csv"
143+
And I send a "GET" request to "/dummy_custom_formats"
144+
Then the response status code should be 200
145+
And the header "Content-Type" should be equal to "text/csv; charset=utf-8"
146+
And the response should be equal to
147+
"""
148+
id,name
149+
1,Kevin
150+
"""

src/Annotation/ApiResource.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
* @Attribute("description", type="string"),
3333
* @Attribute("fetchPartial", type="bool"),
3434
* @Attribute("forceEager", type="bool"),
35+
* @Attribute("formats", type="array"),
3536
* @Attribute("filters", type="string[]"),
3637
* @Attribute("graphql", type="array"),
3738
* @Attribute("iri", type="string"),
@@ -129,6 +130,13 @@ final class ApiResource
129130
*/
130131
private $forceEager;
131132

133+
/**
134+
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
135+
*
136+
* @var array
137+
*/
138+
private $formats;
139+
132140
/**
133141
* @see https://github.com/Haehnchen/idea-php-annotation-plugin/issues/112
134142
*

src/Api/FormatsProvider.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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\Core\Api;
15+
16+
use ApiPlatform\Core\Exception\InvalidArgumentException;
17+
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
18+
19+
/**
20+
* {@inheritdoc}
21+
*
22+
* @author Anthony GRASSIOT <antograssiot@free.fr>
23+
*/
24+
final class FormatsProvider implements FormatsProviderInterface
25+
{
26+
private $configuredFormats;
27+
private $resourceMetadataFactory;
28+
29+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, array $configuredFormats)
30+
{
31+
$this->resourceMetadataFactory = $resourceMetadataFactory;
32+
$this->configuredFormats = $configuredFormats;
33+
}
34+
35+
/**
36+
* {@inheritdoc}
37+
*
38+
* @throws InvalidArgumentException
39+
*/
40+
public function getFormatsFromAttributes(array $attributes): array
41+
{
42+
if (!$attributes || !isset($attributes['resource_class'])) {
43+
return $this->configuredFormats;
44+
}
45+
46+
$resourceMetadata = $this->resourceMetadataFactory->create($attributes['resource_class']);
47+
48+
if (!$formats = $resourceMetadata->getOperationAttribute($attributes, 'formats', [], true)) {
49+
return $this->configuredFormats;
50+
}
51+
52+
if (!\is_array($formats)) {
53+
throw new InvalidArgumentException(sprintf("The 'formats' attributes must be an array, %s given for resource class '%s'.", gettype($formats), $attributes['resource_class']));
54+
}
55+
56+
return $this->getOperationFormats($formats);
57+
}
58+
59+
/**
60+
* Filter and populate the acceptable formats.
61+
*
62+
* @throws InvalidArgumentException
63+
*/
64+
private function getOperationFormats(array $annotationFormats): array
65+
{
66+
$resourceFormats = [];
67+
foreach ($annotationFormats as $format => $value) {
68+
if (!is_numeric($format)) {
69+
$resourceFormats[$format] = (array) $value;
70+
continue;
71+
}
72+
if (!\is_string($value)) {
73+
throw new InvalidArgumentException(sprintf("The 'formats' attributes value must be a string when trying to include an already configured format, %s given.", gettype($value)));
74+
}
75+
if (array_key_exists($value, $this->configuredFormats)) {
76+
$resourceFormats[$value] = $this->configuredFormats[$value];
77+
continue;
78+
}
79+
80+
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.", $value));
81+
}
82+
83+
return $resourceFormats;
84+
}
85+
}

src/Api/FormatsProviderInterface.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
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\Core\Api;
15+
16+
/**
17+
* Extracts formats for a given operation according to the retrieved Metadata.
18+
*
19+
* @author Anthony GRASSIOT <antograssiot@free.fr>
20+
*/
21+
interface FormatsProviderInterface
22+
{
23+
/**
24+
* Finds formats for an operation.
25+
*/
26+
public function getFormatsFromAttributes(array $attributes): array;
27+
}

src/Bridge/Symfony/Bundle/Action/SwaggerUiAction.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@
1313

1414
namespace ApiPlatform\Core\Bridge\Symfony\Bundle\Action;
1515

16+
use ApiPlatform\Core\Api\FormatsProviderInterface;
1617
use ApiPlatform\Core\Documentation\Documentation;
18+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1719
use ApiPlatform\Core\Exception\RuntimeException;
1820
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
1921
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
22+
use ApiPlatform\Core\Util\RequestAttributesExtractor;
2023
use Symfony\Component\HttpFoundation\Request;
2124
use Symfony\Component\HttpFoundation\Response;
2225
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
@@ -37,7 +40,7 @@ final class SwaggerUiAction
3740
private $title;
3841
private $description;
3942
private $version;
40-
private $formats;
43+
private $formats = [];
4144
private $oauthEnabled;
4245
private $oauthClientId;
4346
private $oauthClientSecret;
@@ -46,8 +49,12 @@ final class SwaggerUiAction
4649
private $oauthTokenUrl;
4750
private $oauthAuthorizationUrl;
4851
private $oauthScopes;
52+
private $formatsProvider;
4953

50-
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, \Twig_Environment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', array $formats = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [])
54+
/**
55+
* @throws InvalidArgumentException
56+
*/
57+
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataFactoryInterface $resourceMetadataFactory, NormalizerInterface $normalizer, \Twig_Environment $twig, UrlGeneratorInterface $urlGenerator, string $title = '', string $description = '', string $version = '', /* FormatsProviderInterface */ $formatsProvider = [], $oauthEnabled = false, $oauthClientId = '', $oauthClientSecret = '', $oauthType = '', $oauthFlow = '', $oauthTokenUrl = '', $oauthAuthorizationUrl = '', $oauthScopes = [])
5158
{
5259
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
5360
$this->resourceMetadataFactory = $resourceMetadataFactory;
@@ -57,7 +64,6 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
5764
$this->title = $title;
5865
$this->description = $description;
5966
$this->version = $version;
60-
$this->formats = $formats;
6167
$this->oauthEnabled = $oauthEnabled;
6268
$this->oauthClientId = $oauthClientId;
6369
$this->oauthClientSecret = $oauthClientSecret;
@@ -66,10 +72,30 @@ public function __construct(ResourceNameCollectionFactoryInterface $resourceName
6672
$this->oauthTokenUrl = $oauthTokenUrl;
6773
$this->oauthAuthorizationUrl = $oauthAuthorizationUrl;
6874
$this->oauthScopes = $oauthScopes;
75+
76+
if (\is_array($formatsProvider)) {
77+
if ($formatsProvider) {
78+
// Only trigger notification for non-default argument
79+
@trigger_error('Using an array as formats provider is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED);
80+
}
81+
$this->formats = $formatsProvider;
82+
83+
return;
84+
}
85+
if (!$formatsProvider instanceof FormatsProviderInterface) {
86+
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
87+
}
88+
89+
$this->formatsProvider = $formatsProvider;
6990
}
7091

7192
public function __invoke(Request $request)
7293
{
94+
// BC check to be removed in 3.0
95+
if (null !== $this->formatsProvider) {
96+
$this->formats = $this->formatsProvider->getFormatsFromAttributes(RequestAttributesExtractor::extractAttributes($request));
97+
}
98+
7399
$documentation = new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version, $this->formats);
74100

75101
return new Response($this->twig->render('@ApiPlatform/SwaggerUi/index.html.twig', $this->getContext($request, $documentation)));

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@
6363
</service>
6464
<service id="ApiPlatform\Core\Api\IriConverterInterface" alias="api_platform.iri_converter" />
6565

66+
<service id="api_platform.formats_provider" class="ApiPlatform\Core\Api\FormatsProvider">
67+
<argument type="service" id="api_platform.metadata.resource.metadata_factory"></argument>
68+
<argument>%api_platform.formats%</argument>
69+
</service>
70+
6671
<!-- Serializer -->
6772

6873
<service id="api_platform.serializer.context_builder" class="ApiPlatform\Core\Serializer\SerializerContextBuilder" public="false">
@@ -130,7 +135,7 @@
130135
<!-- kernel.request priority must be < 8 to be executed after the Firewall -->
131136
<service id="api_platform.listener.request.add_format" class="ApiPlatform\Core\EventListener\AddFormatListener">
132137
<argument type="service" id="api_platform.negotiator" />
133-
<argument>%api_platform.formats%</argument>
138+
<argument type="service" id="api_platform.formats_provider"></argument>
134139

135140
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="7" />
136141
</service>
@@ -154,7 +159,7 @@
154159
<service id="api_platform.listener.request.deserialize" class="ApiPlatform\Core\EventListener\DeserializeListener">
155160
<argument type="service" id="api_platform.serializer" />
156161
<argument type="service" id="api_platform.serializer.context_builder" />
157-
<argument>%api_platform.formats%</argument>
162+
<argument type="service" id="api_platform.formats_provider" />
158163

159164
<tag name="kernel.event_listener" event="kernel.request" method="onKernelRequest" priority="2" />
160165
</service>
@@ -205,7 +210,7 @@
205210
<argument>%api_platform.title%</argument>
206211
<argument>%api_platform.description%</argument>
207212
<argument>%api_platform.version%</argument>
208-
<argument>%api_platform.formats%</argument>
213+
<argument type="service" id="api_platform.formats_provider" />
209214
</service>
210215

211216
<service id="api_platform.action.exception" class="ApiPlatform\Core\Action\ExceptionAction" public="true">

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
<argument>%api_platform.title%</argument>
5959
<argument>%api_platform.description%</argument>
6060
<argument>%api_platform.version%</argument>
61-
<argument>%api_platform.formats%</argument>
61+
<argument type="service" id="api_platform.formats_provider"/>
6262
<argument>%api_platform.oauth.enabled%</argument>
6363
<argument>%api_platform.oauth.clientId%</argument>
6464
<argument>%api_platform.oauth.clientSecret%</argument>

src/Documentation/Action/DocumentationAction.php

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@
1313

1414
namespace ApiPlatform\Core\Documentation\Action;
1515

16+
use ApiPlatform\Core\Api\FormatsProviderInterface;
1617
use ApiPlatform\Core\Documentation\Documentation;
18+
use ApiPlatform\Core\Exception\InvalidArgumentException;
1719
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
20+
use ApiPlatform\Core\Util\RequestAttributesExtractor;
1821
use Symfony\Component\HttpFoundation\Request;
1922

2023
/**
@@ -28,15 +31,32 @@ final class DocumentationAction
2831
private $title;
2932
private $description;
3033
private $version;
31-
private $formats;
34+
private $formats = [];
35+
private $formatsProvider;
3236

33-
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, string $title = '', string $description = '', string $version = '', array $formats = [])
37+
/**
38+
* @throws InvalidArgumentException
39+
*/
40+
public function __construct(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, string $title = '', string $description = '', string $version = '', /* FormatsProviderInterface */ $formatsProvider = [])
3441
{
3542
$this->resourceNameCollectionFactory = $resourceNameCollectionFactory;
3643
$this->title = $title;
3744
$this->description = $description;
3845
$this->version = $version;
39-
$this->formats = $formats;
46+
if (\is_array($formatsProvider)) {
47+
if ($formatsProvider) {
48+
// Only trigger notification for non-default argument
49+
@trigger_error('Using an array as formats provider is deprecated since API Platform 2.3 and will not be possible anymore in API Platform 3', E_USER_DEPRECATED);
50+
}
51+
$this->formats = $formatsProvider;
52+
53+
return;
54+
}
55+
if (!$formatsProvider instanceof FormatsProviderInterface) {
56+
throw new InvalidArgumentException(sprintf('The "$formatsProvider" argument is expected to be an implementation of the "%s" interface.', FormatsProviderInterface::class));
57+
}
58+
59+
$this->formatsProvider = $formatsProvider;
4060
}
4161

4262
public function __invoke(Request $request = null): Documentation
@@ -47,6 +67,12 @@ public function __invoke(Request $request = null): Documentation
4767
$context['api_gateway'] = true;
4868
}
4969
$request->attributes->set('_api_normalization_context', $request->attributes->get('_api_normalization_context', []) + $context);
70+
71+
$attributes = RequestAttributesExtractor::extractAttributes($request);
72+
}
73+
// BC check to be removed in 3.0
74+
if (null !== $this->formatsProvider) {
75+
$this->formats = $this->formatsProvider->getFormatsFromAttributes($attributes ?? []);
5076
}
5177

5278
return new Documentation($this->resourceNameCollectionFactory->create(), $this->title, $this->description, $this->version, $this->formats);

0 commit comments

Comments
 (0)