Skip to content

Commit 74ee7ef

Browse files
bpolaszekdunglas
authored andcommitted
Global resource defaults implementation (#3151)
* Add defaults configuration * Suggest defaults in configuration * Apply changes from code review * Add deprecation notices for legacy defaults * Apply @dunglas' suggestions
1 parent dfc1771 commit 74ee7ef

File tree

10 files changed

+263
-18
lines changed

10 files changed

+263
-18
lines changed

src/Annotation/ApiResource.php

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,55 @@ final class ApiResource
7373
{
7474
use AttributesHydratorTrait;
7575

76+
/**
77+
* @internal
78+
*
79+
* @see \ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Configuration::addDefaultsSection
80+
*/
81+
public const CONFIGURABLE_DEFAULTS = [
82+
'accessControl',
83+
'accessControlMessage',
84+
'security',
85+
'securityMessage',
86+
'securityPostDenormalize',
87+
'securityPostDenormalizeMessage',
88+
'cacheHeaders',
89+
'collectionOperations',
90+
'denormalizationContext',
91+
'deprecationReason',
92+
'description',
93+
'elasticsearch',
94+
'fetchPartial',
95+
'forceEager',
96+
'formats',
97+
'filters',
98+
'graphql',
99+
'hydraContext',
100+
'input',
101+
'iri',
102+
'itemOperations',
103+
'mercure',
104+
'messenger',
105+
'normalizationContext',
106+
'openapiContext',
107+
'order',
108+
'output',
109+
'paginationClientEnabled',
110+
'paginationClientItemsPerPage',
111+
'paginationClientPartial',
112+
'paginationEnabled',
113+
'paginationFetchJoinCollection',
114+
'paginationItemsPerPage',
115+
'maximumItemsPerPage',
116+
'paginationMaximumItemsPerPage',
117+
'paginationPartial',
118+
'paginationViaCursor',
119+
'routePrefix',
120+
'sunset',
121+
'swaggerContext',
122+
'validationGroups',
123+
];
124+
76125
/**
77126
* @var string
78127
*/

src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,29 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
191191
if ($config['name_converter']) {
192192
$container->setAlias('api_platform.name_converter', $config['name_converter']);
193193
}
194+
$container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? []));
195+
}
196+
197+
private function normalizeDefaults(array $defaults): array
198+
{
199+
$normalizedDefaults = ['attributes' => []];
200+
$rootLevelOptions = [
201+
'description',
202+
'iri',
203+
'item_operations',
204+
'collection_operations',
205+
'graphql',
206+
];
207+
208+
foreach ($defaults as $option => $value) {
209+
if (\in_array($option, $rootLevelOptions, true)) {
210+
$normalizedDefaults[$option] = $value;
211+
} else {
212+
$normalizedDefaults['attributes'][$option] = $value;
213+
}
214+
}
215+
216+
return $normalizedDefaults;
194217
}
195218

196219
private function registerMetadataConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void

src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php

Lines changed: 80 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

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

16+
use ApiPlatform\Core\Annotation\ApiResource;
1617
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
1718
use ApiPlatform\Core\Exception\FilterValidationException;
1819
use ApiPlatform\Core\Exception\InvalidArgumentException;
@@ -33,6 +34,7 @@
3334
use Symfony\Component\HttpFoundation\Response;
3435
use Symfony\Component\Messenger\MessageBusInterface;
3536
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
37+
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;
3638

3739
/**
3840
* The configuration of the bundle.
@@ -128,13 +130,41 @@ public function getConfigTreeBuilder()
128130
->canBeDisabled()
129131
->addDefaultsIfNotSet()
130132
->children()
131-
->booleanNode('enabled')->defaultTrue()->info('To enable or disable pagination for all resource collections by default.')->end()
132-
->booleanNode('partial')->defaultFalse()->info('To enable or disable partial pagination for all resource collections by default when pagination is enabled.')->end()
133-
->booleanNode('client_enabled')->defaultFalse()->info('To allow the client to enable or disable the pagination.')->end()
134-
->booleanNode('client_items_per_page')->defaultFalse()->info('To allow the client to set the number of items per page.')->end()
135-
->booleanNode('client_partial')->defaultFalse()->info('To allow the client to enable or disable partial pagination.')->end()
136-
->integerNode('items_per_page')->defaultValue(30)->info('The default number of items per page.')->end()
137-
->integerNode('maximum_items_per_page')->defaultNull()->info('The maximum number of items per page.')->end()
133+
->booleanNode('enabled')
134+
->setDeprecated('The use of the `collection.pagination.enabled` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_enabled` instead.')
135+
->defaultTrue()
136+
->info('To enable or disable pagination for all resource collections by default.')
137+
->end()
138+
->booleanNode('partial')
139+
->setDeprecated('The use of the `collection.pagination.partial` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_partial` instead.')
140+
->defaultFalse()
141+
->info('To enable or disable partial pagination for all resource collections by default when pagination is enabled.')
142+
->end()
143+
->booleanNode('client_enabled')
144+
->setDeprecated('The use of the `collection.pagination.client_enabled` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_client_enabled` instead.')
145+
->defaultFalse()
146+
->info('To allow the client to enable or disable the pagination.')
147+
->end()
148+
->booleanNode('client_items_per_page')
149+
->setDeprecated('The use of the `collection.pagination.client_items_per_page` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_client_items_per_page` instead.')
150+
->defaultFalse()
151+
->info('To allow the client to set the number of items per page.')
152+
->end()
153+
->booleanNode('client_partial')
154+
->setDeprecated('The use of the `collection.pagination.client_partial` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_client_partial` instead.')
155+
->defaultFalse()
156+
->info('To allow the client to enable or disable partial pagination.')
157+
->end()
158+
->integerNode('items_per_page')
159+
->setDeprecated('The use of the `collection.pagination.items_per_page` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_items_per_page` instead.')
160+
->defaultValue(30)
161+
->info('The default number of items per page.')
162+
->end()
163+
->integerNode('maximum_items_per_page')
164+
->setDeprecated('The use of the `collection.pagination.maximum_items_per_page` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.pagination_maximum_items_per_page` instead.')
165+
->defaultNull()
166+
->info('The maximum number of items per page.')
167+
->end()
138168
->scalarNode('page_parameter_name')->defaultValue('page')->cannotBeEmpty()->info('The default name of the parameter handling the page number.')->end()
139169
->scalarNode('enabled_parameter_name')->defaultValue('pagination')->cannotBeEmpty()->info('The name of the query parameter to enable or disable pagination.')->end()
140170
->scalarNode('items_per_page_parameter_name')->defaultValue('itemsPerPage')->cannotBeEmpty()->info('The name of the query parameter to set the number of items per page.')->end()
@@ -179,6 +209,8 @@ public function getConfigTreeBuilder()
179209
'jsonld' => ['mime_types' => ['application/ld+json']],
180210
]);
181211

212+
$this->addDefaultsSection($rootNode);
213+
182214
return $treeBuilder;
183215
}
184216

@@ -311,16 +343,30 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void
311343
->arrayNode('http_cache')
312344
->addDefaultsIfNotSet()
313345
->children()
314-
->booleanNode('etag')->defaultTrue()->info('Automatically generate etags for API responses.')->end()
315-
->integerNode('max_age')->defaultNull()->info('Default value for the response max age.')->end()
316-
->integerNode('shared_max_age')->defaultNull()->info('Default value for the response shared (proxy) max age.')->end()
346+
->booleanNode('etag')
347+
->setDeprecated('The use of the `http_cache.etag` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.etag` instead.')
348+
->defaultTrue()
349+
->info('Automatically generate etags for API responses.')
350+
->end()
351+
->integerNode('max_age')
352+
->setDeprecated('The use of the `http_cache.max_age` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.max_age` instead.')
353+
->defaultNull()
354+
->info('Default value for the response max age.')
355+
->end()
356+
->integerNode('shared_max_age')
357+
->setDeprecated('The use of the `http_cache.shared_max_age` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.shared_max_age` instead.')
358+
->defaultNull()
359+
->info('Default value for the response shared (proxy) max age.')
360+
->end()
317361
->arrayNode('vary')
362+
->setDeprecated('The use of the `http_cache.vary` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.vary` instead.')
318363
->defaultValue(['Accept'])
319364
->prototype('scalar')->end()
320365
->info('Default values of the "Vary" HTTP header.')
321366
->end()
322367
->booleanNode('public')->defaultNull()->info('To make all responses public by default.')->end()
323368
->arrayNode('invalidation')
369+
->setDeprecated('The use of the `http_cache.invalidation` has been deprecated in 2.6 and will be removed in 3.0. Use `defaults.cache_headers.invalidation` instead.')
324370
->info('Enable the tags-based cache invalidation system.')
325371
->canBeEnabled()
326372
->children()
@@ -494,4 +540,28 @@ private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, ar
494540
->end()
495541
->end();
496542
}
543+
544+
private function addDefaultsSection(ArrayNodeDefinition $rootNode): void
545+
{
546+
$nameConverter = new CamelCaseToSnakeCaseNameConverter();
547+
$defaultsNode = $rootNode->children()->arrayNode('defaults');
548+
549+
$defaultsNode
550+
->ignoreExtraKeys()
551+
->beforeNormalization()
552+
->always(function (array $defaults) use ($nameConverter) {
553+
$normalizedDefaults = [];
554+
foreach ($defaults as $option => $value) {
555+
$option = $nameConverter->normalize($option);
556+
$normalizedDefaults[$option] = $value;
557+
}
558+
559+
return $normalizedDefaults;
560+
});
561+
562+
foreach (ApiResource::CONFIGURABLE_DEFAULTS as $attribute) {
563+
$snakeCased = $nameConverter->normalize($attribute);
564+
$defaultsNode->children()->variableNode($snakeCased);
565+
}
566+
}
497567
}

src/Bridge/Symfony/Bundle/Resources/config/metadata/annotation.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<service id="api_platform.metadata.resource.metadata_factory.annotation" decorates="api_platform.metadata.resource.metadata_factory" decoration-priority="40" class="ApiPlatform\Core\Metadata\Resource\Factory\AnnotationResourceMetadataFactory" public="false">
1515
<argument type="service" id="annotation_reader" />
1616
<argument type="service" id="api_platform.metadata.resource.metadata_factory.annotation.inner" />
17+
<argument>%api_platform.defaults%</argument>
1718
</service>
1819

1920
<service id="api_platform.metadata.resource.filter_metadata_factory.annotation" decorates="api_platform.metadata.resource.metadata_factory" decoration-priority="20" class="ApiPlatform\Core\Metadata\Resource\Factory\AnnotationResourceFilterMetadataFactory" public="false">

src/Bridge/Symfony/Bundle/Resources/config/metadata/yaml.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<service id="api_platform.metadata.resource.metadata_factory.yaml" decorates="api_platform.metadata.resource.metadata_factory" class="ApiPlatform\Core\Metadata\Resource\Factory\ExtractorResourceMetadataFactory" decoration-priority="40" public="false">
1919
<argument type="service" id="api_platform.metadata.extractor.yaml" />
2020
<argument type="service" id="api_platform.metadata.resource.metadata_factory.yaml.inner" />
21+
<argument>%api_platform.defaults%</argument>
2122
</service>
2223

2324
<service id="api_platform.metadata.property.name_collection_factory.yaml" class="ApiPlatform\Core\Metadata\Property\Factory\ExtractorPropertyNameCollectionFactory" decorates="api_platform.metadata.property.name_collection_factory" public="false">

src/Metadata/Resource/Factory/AnnotationResourceMetadataFactory.php

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ final class AnnotationResourceMetadataFactory implements ResourceMetadataFactory
2727
{
2828
private $reader;
2929
private $decorated;
30+
private $defaults;
3031

31-
public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null)
32+
public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null, array $defaults = [])
3233
{
3334
$this->reader = $reader;
3435
$this->decorated = $decorated;
36+
$this->defaults = $defaults + ['attributes' => []];
3537
}
3638

3739
/**
@@ -78,16 +80,18 @@ private function handleNotFound(?ResourceMetadata $parentPropertyMetadata, strin
7880

7981
private function createMetadata(ApiResource $annotation, ResourceMetadata $parentResourceMetadata = null): ResourceMetadata
8082
{
83+
$attributes = (null === $annotation->attributes && [] === $this->defaults['attributes']) ? null : (array) $annotation->attributes + $this->defaults['attributes'];
84+
8185
if (!$parentResourceMetadata) {
8286
return new ResourceMetadata(
8387
$annotation->shortName,
84-
$annotation->description,
85-
$annotation->iri,
86-
$annotation->itemOperations,
87-
$annotation->collectionOperations,
88-
$annotation->attributes,
88+
$annotation->description ?? $this->defaults['description'] ?? null,
89+
$annotation->iri ?? $this->defaults['iri'] ?? null,
90+
$annotation->itemOperations ?? $this->defaults['item_operations'] ?? null,
91+
$annotation->collectionOperations ?? $this->defaults['collection_operations'] ?? null,
92+
$attributes,
8993
$annotation->subresourceOperations,
90-
$annotation->graphql
94+
$annotation->graphql ?? $this->defaults['graphql'] ?? null
9195
);
9296
}
9397

src/Metadata/Resource/Factory/ExtractorResourceMetadataFactory.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,13 @@ final class ExtractorResourceMetadataFactory implements ResourceMetadataFactoryI
2727
{
2828
private $extractor;
2929
private $decorated;
30+
private $defaults;
3031

31-
public function __construct(ExtractorInterface $extractor, ResourceMetadataFactoryInterface $decorated = null)
32+
public function __construct(ExtractorInterface $extractor, ResourceMetadataFactoryInterface $decorated = null, array $defaults = [])
3233
{
3334
$this->extractor = $extractor;
3435
$this->decorated = $decorated;
36+
$this->defaults = $defaults + ['attributes' => []];
3537
}
3638

3739
/**
@@ -52,6 +54,13 @@ public function create(string $resourceClass): ResourceMetadata
5254
return $this->handleNotFound($parentResourceMetadata, $resourceClass);
5355
}
5456

57+
$resource['description'] = $resource['description'] ?? $this->defaults['description'] ?? null;
58+
$resource['iri'] = $resource['iri'] ?? $this->defaults['iri'] ?? null;
59+
$resource['itemOperations'] = $resource['itemOperations'] ?? $this->defaults['item_operations'] ?? null;
60+
$resource['collectionOperations'] = $resource['collectionOperations'] ?? $this->defaults['collection_operations'] ?? null;
61+
$resource['graphql'] = $resource['graphql'] ?? $this->defaults['graphql'] ?? null;
62+
$resource['attributes'] = (null === $resource['attributes'] && [] === $this->defaults['attributes']) ? null : (array) $resource['attributes'] + $this->defaults['attributes'];
63+
5564
return $this->update($parentResourceMetadata ?: new ResourceMetadata(), $resource);
5665
}
5766

tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,9 @@ class ApiPlatformExtensionTest extends TestCase
144144
'doctrine_mongodb_odm' => [
145145
'enabled' => false,
146146
],
147+
'defaults' => [
148+
'attributes' => [],
149+
],
147150
]];
148151

149152
private $extension;
@@ -694,6 +697,7 @@ public function testEnableElasticsearch()
694697
$containerBuilderProphecy->registerForAutoconfiguration(RequestBodySearchCollectionExtensionInterface::class)->willReturn($this->childDefinitionProphecy)->shouldBeCalled();
695698
$containerBuilderProphecy->setParameter('api_platform.elasticsearch.hosts', ['http://elasticsearch:9200'])->shouldBeCalled();
696699
$containerBuilderProphecy->setParameter('api_platform.elasticsearch.mapping', [])->shouldBeCalled();
700+
$containerBuilderProphecy->setParameter('api_platform.defaults', ['attributes' => []])->shouldBeCalled();
697701

698702
$config = self::DEFAULT_CONFIG;
699703
$config['api_platform']['elasticsearch'] = [
@@ -804,6 +808,7 @@ private function getPartialContainerBuilderProphecy()
804808
'api_platform.http_cache.shared_max_age' => null,
805809
'api_platform.http_cache.vary' => ['Accept'],
806810
'api_platform.http_cache.public' => null,
811+
'api_platform.defaults' => ['attributes' => []],
807812
'api_platform.enable_entrypoint' => true,
808813
'api_platform.enable_docs' => true,
809814
];
@@ -1076,6 +1081,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
10761081
'api_platform.resource_class_directories' => Argument::type('array'),
10771082
'api_platform.validator.serialize_payload_fields' => [],
10781083
'api_platform.elasticsearch.enabled' => false,
1084+
'api_platform.defaults' => ['attributes' => []],
10791085
];
10801086

10811087
foreach ($parameters as $key => $value) {

0 commit comments

Comments
 (0)