Skip to content
Merged
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
49 changes: 49 additions & 0 deletions src/Annotation/ApiResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,55 @@ final class ApiResource
{
use AttributesHydratorTrait;

/**
* @internal
*
* @see \ApiPlatform\Core\Bridge\Symfony\Bundle\DependencyInjection\Configuration::addDefaultsSection
*/
public const CONFIGURABLE_DEFAULTS = [
'accessControl',
'accessControlMessage',
'security',
'securityMessage',
'securityPostDenormalize',
'securityPostDenormalizeMessage',
'cacheHeaders',
'collectionOperations',
'denormalizationContext',
'deprecationReason',
'description',
'elasticsearch',
'fetchPartial',
'forceEager',
'formats',
'filters',
'graphql',
'hydraContext',
'input',
'iri',
'itemOperations',
'mercure',
'messenger',
'normalizationContext',
'openapiContext',
'order',
'output',
'paginationClientEnabled',
'paginationClientItemsPerPage',
'paginationClientPartial',
'paginationEnabled',
'paginationFetchJoinCollection',
'paginationItemsPerPage',
'maximumItemsPerPage',
'paginationMaximumItemsPerPage',
'paginationPartial',
'paginationViaCursor',
'routePrefix',
'sunset',
'swaggerContext',
'validationGroups',
];

/**
* @var string
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,29 @@ private function registerCommonConfiguration(ContainerBuilder $container, array
if ($config['name_converter']) {
$container->setAlias('api_platform.name_converter', $config['name_converter']);
}
$container->setParameter('api_platform.defaults', $this->normalizeDefaults($config['defaults'] ?? []));
}

private function normalizeDefaults(array $defaults): array
{
$normalizedDefaults = ['attributes' => []];
$rootLevelOptions = [
'description',
'iri',
'item_operations',
'collection_operations',
'graphql',
];

foreach ($defaults as $option => $value) {
if (\in_array($option, $rootLevelOptions, true)) {
$normalizedDefaults[$option] = $value;
} else {
$normalizedDefaults['attributes'][$option] = $value;
}
}

return $normalizedDefaults;
}

private function registerMetadataConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void
Expand Down
90 changes: 80 additions & 10 deletions src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

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

use ApiPlatform\Core\Annotation\ApiResource;
use ApiPlatform\Core\Bridge\Elasticsearch\Metadata\Document\DocumentMetadata;
use ApiPlatform\Core\Exception\FilterValidationException;
use ApiPlatform\Core\Exception\InvalidArgumentException;
Expand All @@ -33,6 +34,7 @@
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Serializer\Exception\ExceptionInterface as SerializerExceptionInterface;
use Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter;

/**
* The configuration of the bundle.
Expand Down Expand Up @@ -128,13 +130,41 @@ public function getConfigTreeBuilder()
->canBeDisabled()
->addDefaultsIfNotSet()
->children()
->booleanNode('enabled')->defaultTrue()->info('To enable or disable pagination for all resource collections by default.')->end()
->booleanNode('partial')->defaultFalse()->info('To enable or disable partial pagination for all resource collections by default when pagination is enabled.')->end()
->booleanNode('client_enabled')->defaultFalse()->info('To allow the client to enable or disable the pagination.')->end()
->booleanNode('client_items_per_page')->defaultFalse()->info('To allow the client to set the number of items per page.')->end()
->booleanNode('client_partial')->defaultFalse()->info('To allow the client to enable or disable partial pagination.')->end()
->integerNode('items_per_page')->defaultValue(30)->info('The default number of items per page.')->end()
->integerNode('maximum_items_per_page')->defaultNull()->info('The maximum number of items per page.')->end()
->booleanNode('enabled')
->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.')
->defaultTrue()
->info('To enable or disable pagination for all resource collections by default.')
->end()
->booleanNode('partial')
->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.')
->defaultFalse()
->info('To enable or disable partial pagination for all resource collections by default when pagination is enabled.')
->end()
->booleanNode('client_enabled')
->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.')
->defaultFalse()
->info('To allow the client to enable or disable the pagination.')
->end()
->booleanNode('client_items_per_page')
->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.')
->defaultFalse()
->info('To allow the client to set the number of items per page.')
->end()
->booleanNode('client_partial')
->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.')
->defaultFalse()
->info('To allow the client to enable or disable partial pagination.')
->end()
->integerNode('items_per_page')
->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.')
->defaultValue(30)
->info('The default number of items per page.')
->end()
->integerNode('maximum_items_per_page')
->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.')
->defaultNull()
->info('The maximum number of items per page.')
->end()
->scalarNode('page_parameter_name')->defaultValue('page')->cannotBeEmpty()->info('The default name of the parameter handling the page number.')->end()
->scalarNode('enabled_parameter_name')->defaultValue('pagination')->cannotBeEmpty()->info('The name of the query parameter to enable or disable pagination.')->end()
->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()
Expand Down Expand Up @@ -179,6 +209,8 @@ public function getConfigTreeBuilder()
'jsonld' => ['mime_types' => ['application/ld+json']],
]);

$this->addDefaultsSection($rootNode);

return $treeBuilder;
}

Expand Down Expand Up @@ -311,16 +343,30 @@ private function addHttpCacheSection(ArrayNodeDefinition $rootNode): void
->arrayNode('http_cache')
->addDefaultsIfNotSet()
->children()
->booleanNode('etag')->defaultTrue()->info('Automatically generate etags for API responses.')->end()
->integerNode('max_age')->defaultNull()->info('Default value for the response max age.')->end()
->integerNode('shared_max_age')->defaultNull()->info('Default value for the response shared (proxy) max age.')->end()
->booleanNode('etag')
->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.')
->defaultTrue()
->info('Automatically generate etags for API responses.')
->end()
->integerNode('max_age')
->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.')
->defaultNull()
->info('Default value for the response max age.')
->end()
->integerNode('shared_max_age')
->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.')
->defaultNull()
->info('Default value for the response shared (proxy) max age.')
->end()
->arrayNode('vary')
->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.')
->defaultValue(['Accept'])
->prototype('scalar')->end()
->info('Default values of the "Vary" HTTP header.')
->end()
->booleanNode('public')->defaultNull()->info('To make all responses public by default.')->end()
->arrayNode('invalidation')
->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.')
->info('Enable the tags-based cache invalidation system.')
->canBeEnabled()
->children()
Expand Down Expand Up @@ -494,4 +540,28 @@ private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, ar
->end()
->end();
}

private function addDefaultsSection(ArrayNodeDefinition $rootNode): void
{
$nameConverter = new CamelCaseToSnakeCaseNameConverter();
$defaultsNode = $rootNode->children()->arrayNode('defaults');

$defaultsNode
->ignoreExtraKeys()
->beforeNormalization()
->always(function (array $defaults) use ($nameConverter) {
$normalizedDefaults = [];
foreach ($defaults as $option => $value) {
$option = $nameConverter->normalize($option);
$normalizedDefaults[$option] = $value;
}

return $normalizedDefaults;
});

foreach (ApiResource::CONFIGURABLE_DEFAULTS as $attribute) {
$snakeCased = $nameConverter->normalize($attribute);
$defaultsNode->children()->variableNode($snakeCased);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<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">
<argument type="service" id="annotation_reader" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory.annotation.inner" />
<argument>%api_platform.defaults%</argument>
</service>

<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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<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">
<argument type="service" id="api_platform.metadata.extractor.yaml" />
<argument type="service" id="api_platform.metadata.resource.metadata_factory.yaml.inner" />
<argument>%api_platform.defaults%</argument>
</service>

<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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ final class AnnotationResourceMetadataFactory implements ResourceMetadataFactory
{
private $reader;
private $decorated;
private $defaults;

public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null)
public function __construct(Reader $reader, ResourceMetadataFactoryInterface $decorated = null, array $defaults = [])
{
$this->reader = $reader;
$this->decorated = $decorated;
$this->defaults = $defaults + ['attributes' => []];
}

/**
Expand Down Expand Up @@ -78,16 +80,18 @@ private function handleNotFound(?ResourceMetadata $parentPropertyMetadata, strin

private function createMetadata(ApiResource $annotation, ResourceMetadata $parentResourceMetadata = null): ResourceMetadata
{
$attributes = (null === $annotation->attributes && [] === $this->defaults['attributes']) ? null : (array) $annotation->attributes + $this->defaults['attributes'];

if (!$parentResourceMetadata) {
return new ResourceMetadata(
$annotation->shortName,
$annotation->description,
$annotation->iri,
$annotation->itemOperations,
$annotation->collectionOperations,
$annotation->attributes,
$annotation->description ?? $this->defaults['description'] ?? null,
$annotation->iri ?? $this->defaults['iri'] ?? null,
$annotation->itemOperations ?? $this->defaults['item_operations'] ?? null,
$annotation->collectionOperations ?? $this->defaults['collection_operations'] ?? null,
$attributes,
$annotation->subresourceOperations,
$annotation->graphql
$annotation->graphql ?? $this->defaults['graphql'] ?? null
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ final class ExtractorResourceMetadataFactory implements ResourceMetadataFactoryI
{
private $extractor;
private $decorated;
private $defaults;

public function __construct(ExtractorInterface $extractor, ResourceMetadataFactoryInterface $decorated = null)
public function __construct(ExtractorInterface $extractor, ResourceMetadataFactoryInterface $decorated = null, array $defaults = [])
{
$this->extractor = $extractor;
$this->decorated = $decorated;
$this->defaults = $defaults + ['attributes' => []];
}

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

$resource['description'] = $resource['description'] ?? $this->defaults['description'] ?? null;
$resource['iri'] = $resource['iri'] ?? $this->defaults['iri'] ?? null;
$resource['itemOperations'] = $resource['itemOperations'] ?? $this->defaults['item_operations'] ?? null;
$resource['collectionOperations'] = $resource['collectionOperations'] ?? $this->defaults['collection_operations'] ?? null;
$resource['graphql'] = $resource['graphql'] ?? $this->defaults['graphql'] ?? null;
$resource['attributes'] = (null === $resource['attributes'] && [] === $this->defaults['attributes']) ? null : (array) $resource['attributes'] + $this->defaults['attributes'];

return $this->update($parentResourceMetadata ?: new ResourceMetadata(), $resource);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,9 @@ class ApiPlatformExtensionTest extends TestCase
'doctrine_mongodb_odm' => [
'enabled' => false,
],
'defaults' => [
'attributes' => [],
],
]];

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

$config = self::DEFAULT_CONFIG;
$config['api_platform']['elasticsearch'] = [
Expand Down Expand Up @@ -804,6 +808,7 @@ private function getPartialContainerBuilderProphecy()
'api_platform.http_cache.shared_max_age' => null,
'api_platform.http_cache.vary' => ['Accept'],
'api_platform.http_cache.public' => null,
'api_platform.defaults' => ['attributes' => []],
'api_platform.enable_entrypoint' => true,
'api_platform.enable_docs' => true,
];
Expand Down Expand Up @@ -1076,6 +1081,7 @@ private function getBaseContainerBuilderProphecy(array $doctrineIntegrationsToLo
'api_platform.resource_class_directories' => Argument::type('array'),
'api_platform.validator.serialize_payload_fields' => [],
'api_platform.elasticsearch.enabled' => false,
'api_platform.defaults' => ['attributes' => []],
];

foreach ($parameters as $key => $value) {
Expand Down
Loading