diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index cf39090b833..8b5ef60d791 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -191,6 +191,7 @@ private function registerCommonConfiguration(ContainerBuilder $container, array if ($config['name_converter']) { $container->setAlias('api_platform.name_converter', $config['name_converter']); } + $container->setParameter('api_platform.defaults', $config['defaults']); } private function registerMetadataConfiguration(ContainerBuilder $container, array $config, XmlFileLoader $loader): void diff --git a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php index b49c43b28ed..01a66ee3b80 100644 --- a/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php +++ b/src/Bridge/Symfony/Bundle/DependencyInjection/Configuration.php @@ -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; @@ -179,6 +180,8 @@ public function getConfigTreeBuilder() 'jsonld' => ['mime_types' => ['application/ld+json']], ]); + $this->addDefaultsSection($rootNode); + return $treeBuilder; } @@ -494,4 +497,21 @@ private function addFormatSection(ArrayNodeDefinition $rootNode, string $key, ar ->end() ->end(); } + + private function addDefaultsSection(ArrayNodeDefinition $rootNode): void + { + $defaultsNode = $rootNode->children()->arrayNode('defaults')->addDefaultsIfNotSet(); + $attributesNode = $defaultsNode->children()->arrayNode('attributes')->addDefaultsIfNotSet(); + $properties = (new \ReflectionClass(ApiResource::class))->getProperties(); + + foreach ($properties as $property) { + $optionName = $property->getName(); + if ('attributes' === $optionName) { // "attributes" is a sub-level and not a config option + continue; + } + + $node = $property->isPublic() ? $defaultsNode : $attributesNode; + $node->children()->variableNode($optionName); + } + } } diff --git a/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml b/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml index 1a50a9b0e14..b9ef44f00c2 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/metadata/metadata.xml @@ -33,6 +33,11 @@ %api_platform.patch_formats% + + + %api_platform.defaults% + + diff --git a/src/Metadata/Resource/Factory/DefaultsResourceMetadataFactory.php b/src/Metadata/Resource/Factory/DefaultsResourceMetadataFactory.php new file mode 100644 index 00000000000..7d490ee51f4 --- /dev/null +++ b/src/Metadata/Resource/Factory/DefaultsResourceMetadataFactory.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Metadata\Resource\Factory; + +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; + +/** + * Creates a resource metadata from the default configuration. + * + * @author Beno!t POLASZEK + */ +final class DefaultsResourceMetadataFactory implements ResourceMetadataFactoryInterface +{ + private $decorated; + + private $defaults; + + public function __construct(ResourceMetadataFactoryInterface $decorated, array $defaults) + { + $this->decorated = $decorated; + $this->defaults = $defaults; + } + + /** + * {@inheritdoc} + */ + public function create(string $resourceClass): ResourceMetadata + { + $resourceMetadata = $this->decorated->create($resourceClass); + $attributes = $resourceMetadata->getAttributes(); + $attributesConfig = $this->defaults['attributes'] ?? []; + + foreach ($this->defaults as $key => $value) { + if ('attributes' === $key) { + continue; + } + + if (method_exists($resourceMetadata, 'get'.$key) && null === $resourceMetadata->{'get'.$key}() && method_exists($resourceMetadata, 'with'.$key)) { + $resourceMetadata = $resourceMetadata->{'with'.$key}($value); + } + } + + foreach ($attributesConfig as $key => $value) { + if (\array_key_exists($key, $attributes)) { + continue; + } + + $attributes[$key] = $value; + $resourceMetadata = $resourceMetadata->withAttributes($attributes); + } + + return $resourceMetadata; + } +} diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php index 11f23d41b3f..7bb63186306 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ApiPlatformExtensionTest.php @@ -144,6 +144,9 @@ class ApiPlatformExtensionTest extends TestCase 'doctrine_mongodb_odm' => [ 'enabled' => false, ], + 'defaults' => [ + 'attributes' => [], + ], ]]; private $extension; @@ -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'] = [ @@ -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, ]; @@ -885,6 +890,7 @@ private function getPartialContainerBuilderProphecy() 'api_platform.metadata.property.name_collection_factory.property_info', 'api_platform.metadata.property.name_collection_factory.xml', 'api_platform.metadata.resource.metadata_factory.cached', + 'api_platform.metadata.resource.metadata_factory.defaults', 'api_platform.metadata.resource.metadata_factory.operation', 'api_platform.metadata.resource.metadata_factory.formats', 'api_platform.metadata.resource.metadata_factory.input_output', @@ -1076,6 +1082,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) { diff --git a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php index a891c8724e2..39e07400a81 100644 --- a/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php +++ b/tests/Bridge/Symfony/Bundle/DependencyInjection/ConfigurationTest.php @@ -204,6 +204,9 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm ], 'allow_plain_identifiers' => false, 'resource_class_directories' => [], + 'defaults' => [ + 'attributes' => [], + ], ], $config); } @@ -402,4 +405,21 @@ public function testEnableElasticsearch() $this->assertTrue($config['elasticsearch']['enabled']); } + + public function testDefaultsConfig() + { + $config = $this->processor->processConfiguration($this->configuration, [ + 'api_platform' => [ + 'defaults' => [ + 'description' => 'foo', + 'attributes' => [ + 'mercure' => true, + ], + ], + ], + ]); + + $this->assertEquals('foo', $config['defaults']['description']); + $this->assertTrue($config['defaults']['attributes']['mercure']); + } } diff --git a/tests/Metadata/Resource/Factory/DefaultsResourceMetadataFactoryTest.php b/tests/Metadata/Resource/Factory/DefaultsResourceMetadataFactoryTest.php new file mode 100644 index 00000000000..98b743185d5 --- /dev/null +++ b/tests/Metadata/Resource/Factory/DefaultsResourceMetadataFactoryTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +declare(strict_types=1); + +namespace ApiPlatform\Core\Tests\Metadata\Resource\Factory; + +use ApiPlatform\Core\Metadata\Resource\Factory\DefaultsResourceMetadataFactory; +use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; +use ApiPlatform\Core\Metadata\Resource\ResourceMetadata; +use ApiPlatform\Core\Tests\Fixtures\TestBundle\Entity\Dummy; +use PHPUnit\Framework\TestCase; + +class DefaultsResourceMetadataFactoryTest extends TestCase +{ + public function testCreate(): void + { + $decoratedProphecy = $this->prophesize(ResourceMetadataFactoryInterface::class); + $decoratedProphecy->create(Dummy::class)->shouldBeCalled()->willReturn(new ResourceMetadata( + 'foo', + null, + null, + null, + null, + [ + 'mercure' => true, + ] + )); + + $defaults = [ + 'shortName' => 'bar', + 'description' => 'A Foo entity', + 'attributes' => [ + 'mercure' => false, + 'messenger' => true, + ], + ]; + + $factory = new DefaultsResourceMetadataFactory($decoratedProphecy->reveal(), $defaults); + + $resourceMetadata = $factory->create(Dummy::class); + $this->assertEquals('foo', $resourceMetadata->getShortName()); + $this->assertEquals('A Foo entity', $resourceMetadata->getDescription()); + $this->assertTrue($resourceMetadata->getAttribute('mercure')); + $this->assertTrue($resourceMetadata->getAttribute('messenger')); + } +}