diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index 2903eb5557..08049825f7 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -54,6 +54,7 @@ public function __construct( private readonly UrlGeneratorInterface $urlGenerator, private readonly ?NameConverterInterface $nameConverter = null, private readonly ?array $defaultContext = [], + private readonly ?bool $entrypointEnabled = true, ) { } @@ -443,19 +444,21 @@ private function isSingleRelation(ApiProperty $propertyMetadata): bool */ private function getClasses(array $entrypointProperties, array $classes, string $hydraPrefix = ContextBuilder::HYDRA_PREFIX): array { - $classes[] = [ - '@id' => '#Entrypoint', - '@type' => $hydraPrefix.'Class', - $hydraPrefix.'title' => 'Entrypoint', - $hydraPrefix.'supportedProperty' => $entrypointProperties, - $hydraPrefix.'supportedOperation' => [ - '@type' => $hydraPrefix.'Operation', - $hydraPrefix.'method' => 'GET', - $hydraPrefix.'title' => 'index', - $hydraPrefix.'description' => 'The API Entrypoint.', - $hydraPrefix.'returns' => 'Entrypoint', - ], - ]; + if ($this->entrypointEnabled) { + $classes[] = [ + '@id' => '#Entrypoint', + '@type' => $hydraPrefix.'Class', + $hydraPrefix.'title' => 'Entrypoint', + $hydraPrefix.'supportedProperty' => $entrypointProperties, + $hydraPrefix.'supportedOperation' => [ + '@type' => $hydraPrefix.'Operation', + $hydraPrefix.'method' => 'GET', + $hydraPrefix.'title' => 'index', + $hydraPrefix.'description' => 'The API Entrypoint.', + $hydraPrefix.'returns' => 'Entrypoint', + ], + ]; + } $classes[] = [ '@id' => '#ConstraintViolationList', @@ -560,7 +563,9 @@ private function computeDoc(Documentation $object, array $classes, string $hydra $doc[$hydraPrefix.'description'] = $object->getDescription(); } - $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); + if ($this->entrypointEnabled) { + $doc[$hydraPrefix.'entrypoint'] = $this->urlGenerator->generate('api_entrypoint'); + } $doc[$hydraPrefix.'supportedClass'] = $classes; return $doc; diff --git a/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php index 142acf2d32..56bff521b2 100644 --- a/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php @@ -824,4 +824,106 @@ public function testNormalizeWithoutPrefix(): void $this->assertEquals($expected, $documentationNormalizer->normalize($documentation, null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])); } + + public function testNormalizeNoEntrypointAndHideHydraOperation(): void + { + $title = 'Test Api'; + $desc = 'test ApiGerard'; + $version = '0.0.0'; + $documentation = new Documentation(new ResourceNameCollection(['dummy' => 'dummy']), $title, $desc, $version); + + $resourceMetadataFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataFactoryProphecy->create('dummy')->willReturn(new ResourceMetadataCollection('dummy', [ + (new ApiResource())->withHideHydraOperation(true)->withOperations(new Operations([ + 'get' => new Get(), + ])), + ])); + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $urlGenerator = $this->prophesize(UrlGeneratorInterface::class); + $urlGenerator->generate('api_doc', ['_format' => 'jsonld'], Argument::any())->willReturn('/doc'); + + $documentationNormalizer = new DocumentationNormalizer( + $resourceMetadataFactoryProphecy->reveal(), + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $urlGenerator->reveal(), + new CustomConverter(), + [], + false, + ); + + $expected = [ + '@context' => [ + HYDRA_CONTEXT, + [ + '@vocab' => '/doc#', + 'hydra' => 'http://www.w3.org/ns/hydra/core#', + 'rdf' => 'http://www.w3.org/1999/02/22-rdf-syntax-ns#', + 'rdfs' => 'http://www.w3.org/2000/01/rdf-schema#', + 'xmls' => 'http://www.w3.org/2001/XMLSchema#', + 'owl' => 'http://www.w3.org/2002/07/owl#', + 'schema' => 'https://schema.org/', + 'domain' => [ + '@id' => 'rdfs:domain', + '@type' => '@id', + ], + 'range' => [ + '@id' => 'rdfs:range', + '@type' => '@id', + ], + 'subClassOf' => [ + '@id' => 'rdfs:subClassOf', + '@type' => '@id', + ], + ], + ], + '@id' => '/doc', + '@type' => 'ApiDocumentation', + 'title' => 'Test Api', + 'description' => 'test ApiGerard', + 'supportedClass' => [ + [ + '@id' => '#ConstraintViolationList', + '@type' => 'Class', + 'title' => 'ConstraintViolationList', + 'description' => 'A constraint violation List.', + 'supportedProperty' => [ + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#ConstraintViolationList/propertyPath', + '@type' => 'rdf:Property', + 'rdfs:label' => 'propertyPath', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'title' => 'propertyPath', + 'description' => 'The property path of the violation', + 'readable' => true, + 'writeable' => false, + ], + [ + '@type' => 'SupportedProperty', + 'property' => [ + '@id' => '#ConstraintViolationList/message', + '@type' => 'rdf:Property', + 'rdfs:label' => 'message', + 'domain' => '#ConstraintViolationList', + 'range' => 'xmls:string', + ], + 'title' => 'message', + 'description' => 'The message associated with the violation', + 'readable' => true, + 'writeable' => false, + ], + ], + ], + ], + ]; + + $this->assertEquals($expected, $documentationNormalizer->normalize($documentation, null, [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false])); + } } diff --git a/src/Laravel/ApiPlatformProvider.php b/src/Laravel/ApiPlatformProvider.php index f9a42c9047..475e51242b 100644 --- a/src/Laravel/ApiPlatformProvider.php +++ b/src/Laravel/ApiPlatformProvider.php @@ -731,6 +731,7 @@ public function register(): void $this->app->singleton(HydraDocumentationNormalizer::class, function (Application $app) { $config = $app['config']; $defaultContext = $config->get('api-platform.serializer', []); + $entrypointEnabled = $config->get('api-platform.enable_entrypoint', true); return new HydraDocumentationNormalizer( $app->make(ResourceMetadataCollectionFactoryInterface::class), @@ -739,7 +740,8 @@ public function register(): void $app->make(ResourceClassResolverInterface::class), $app->make(UrlGeneratorInterface::class), $app->make(NameConverterInterface::class), - $defaultContext + $defaultContext, + $entrypointEnabled ); }); diff --git a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php index 4e273866bd..4bcf3e5a5e 100644 --- a/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php +++ b/src/Symfony/Bundle/DependencyInjection/ApiPlatformExtension.php @@ -112,6 +112,14 @@ public function load(array $configs, ContainerBuilder $container): void $patchFormats = $this->getFormats($config['patch_formats']); $errorFormats = $this->getFormats($config['error_formats']); $docsFormats = $this->getFormats($config['docs_formats']); + if (!$config['enable_docs']) { + // JSON-LD documentation format is mandatory, even if documentation is disabled. + $docsFormats = isset($formats['jsonld']) ? ['jsonld' => ['application/ld+json']] : []; + // If documentation is disabled, the Hydra documentation for all the resources is hidden by default. + if (!isset($config['defaults']['hideHydraOperation']) && !isset($config['defaults']['hide_hydra_operation'])) { + $config['defaults']['hideHydraOperation'] = true; + } + } $jsonSchemaFormats = $config['jsonschema_formats']; if (!$jsonSchemaFormats) { @@ -538,11 +546,6 @@ private function registerJsonLdHydraConfiguration(ContainerBuilder $container, a if (!$container->has('api_platform.json_schema.schema_factory')) { $container->removeDefinition('api_platform.hydra.json_schema.schema_factory'); } - - if (!$config['enable_docs']) { - $container->removeDefinition('api_platform.hydra.listener.response.add_link_header'); - $container->removeDefinition('api_platform.hydra.processor.link'); - } } private function registerJsonHalConfiguration(array $formats, XmlFileLoader $loader): void diff --git a/src/Symfony/Bundle/Resources/config/hydra.xml b/src/Symfony/Bundle/Resources/config/hydra.xml index 021b885d3b..6fb156141a 100644 --- a/src/Symfony/Bundle/Resources/config/hydra.xml +++ b/src/Symfony/Bundle/Resources/config/hydra.xml @@ -17,6 +17,7 @@ %api_platform.serializer.default_context% + %api_platform.enable_entrypoint% diff --git a/src/Symfony/Routing/ApiLoader.php b/src/Symfony/Routing/ApiLoader.php index f71206cbe5..6da29ed775 100644 --- a/src/Symfony/Routing/ApiLoader.php +++ b/src/Symfony/Routing/ApiLoader.php @@ -36,7 +36,7 @@ final class ApiLoader extends Loader private readonly XmlFileLoader $fileLoader; - public function __construct(KernelInterface $kernel, private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ContainerInterface $container, private readonly array $formats, private readonly array $resourceClassDirectories = [], private readonly bool $graphqlEnabled = false, private readonly bool $entrypointEnabled = true, private readonly bool $docsEnabled = true, private readonly bool $graphiQlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false) + public function __construct(KernelInterface $kernel, private readonly ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, private readonly ContainerInterface $container, private readonly array $formats, private readonly array $resourceClassDirectories = [], private readonly bool $graphqlEnabled = false, private readonly bool $entrypointEnabled = true, readonly bool $docsEnabled = true, private readonly bool $graphiQlEnabled = false, private readonly bool $graphQlPlaygroundEnabled = false) { /** @var string[]|string $paths */ $paths = $kernel->locateResource('@ApiPlatformBundle/Resources/config/routing'); @@ -124,6 +124,7 @@ public function supports(mixed $resource, ?string $type = null): bool */ private function loadExternalFiles(RouteCollection $routeCollection): void { + $routeCollection->addCollection($this->fileLoader->load('docs.xml')); $routeCollection->addCollection($this->fileLoader->load('genid.xml')); $routeCollection->addCollection($this->fileLoader->load('errors.xml')); @@ -131,10 +132,6 @@ private function loadExternalFiles(RouteCollection $routeCollection): void $routeCollection->addCollection($this->fileLoader->load('api.xml')); } - if ($this->docsEnabled) { - $routeCollection->addCollection($this->fileLoader->load('docs.xml')); - } - if ($this->graphqlEnabled) { $graphqlCollection = $this->fileLoader->load('graphql/graphql.xml'); $graphqlCollection->addDefaults(['_graphql' => true]);