diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index 510e5f48c56..55a41b915a2 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -75,6 +75,10 @@ public function normalize(mixed $object, ?string $format = null, array $context continue; } + if (true === $resourceMetadata->getHideHydraOperation()) { + continue; + } + $shortName = $resourceMetadata->getShortName(); $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName"; @@ -243,6 +247,10 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio $hydraOperations = []; foreach ($resourceMetadataCollection as $resourceMetadata) { foreach ($resourceMetadata->getOperations() as $operation) { + if (true === $operation->getHideHydraOperation()) { + continue; + } + if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) { continue; } diff --git a/src/Hydra/Serializer/EntrypointNormalizer.php b/src/Hydra/Serializer/EntrypointNormalizer.php index 19bf8574a29..199ca64ff42 100644 --- a/src/Hydra/Serializer/EntrypointNormalizer.php +++ b/src/Hydra/Serializer/EntrypointNormalizer.php @@ -56,7 +56,7 @@ public function normalize(mixed $object, ?string $format = null, array $context foreach ($resource->getOperations() as $operation) { $key = lcfirst($resource->getShortName()); - if (!$operation instanceof CollectionOperationInterface || isset($entrypoint[$key])) { + if (true === $operation->getHideHydraOperation() || !$operation instanceof CollectionOperationInterface || isset($entrypoint[$key])) { continue; } diff --git a/src/Metadata/ApiResource.php b/src/Metadata/ApiResource.php index 154dca50e32..c420e97c644 100644 --- a/src/Metadata/ApiResource.php +++ b/src/Metadata/ApiResource.php @@ -964,6 +964,7 @@ public function __construct( array|string|null $middleware = null, array|Parameters|null $parameters = null, protected ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, protected array $extraProperties = [], ) { parent::__construct( @@ -1009,6 +1010,7 @@ class: $class, policy: $policy, middleware: $middleware, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties ); diff --git a/src/Metadata/Delete.php b/src/Metadata/Delete.php index 12c9ccb3337..7ee88218419 100644 --- a/src/Metadata/Delete.php +++ b/src/Metadata/Delete.php @@ -99,6 +99,7 @@ public function __construct( ?string $policy = null, array|string|null $middleware = null, ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, array $extraProperties = [], ) { parent::__construct( @@ -180,6 +181,7 @@ class: $class, collectDenormalizationErrors: $collectDenormalizationErrors, parameters: $parameters, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, stateOptions: $stateOptions, ); } diff --git a/src/Metadata/Extractor/XmlResourceExtractor.php b/src/Metadata/Extractor/XmlResourceExtractor.php index 86c9f4fad07..3e36ff5f86d 100644 --- a/src/Metadata/Extractor/XmlResourceExtractor.php +++ b/src/Metadata/Extractor/XmlResourceExtractor.php @@ -95,6 +95,7 @@ private function buildExtendedBase(\SimpleXMLElement $resource): array 'exceptionToStatus' => $this->buildExceptionToStatus($resource), 'queryParameterValidationEnabled' => $this->phpize($resource, 'queryParameterValidationEnabled', 'bool'), 'strictQueryParameterValidation' => $this->phpize($resource, 'strictQueryParameterValidation', 'bool'), + 'hideHydraOperation' => $this->phpize($resource, 'hideHydraOperation', 'bool'), 'stateOptions' => $this->buildStateOptions($resource), 'links' => $this->buildLinks($resource), 'headers' => $this->buildHeaders($resource), diff --git a/src/Metadata/Extractor/YamlResourceExtractor.php b/src/Metadata/Extractor/YamlResourceExtractor.php index 16dcbd7ff13..45fc6a29db0 100644 --- a/src/Metadata/Extractor/YamlResourceExtractor.php +++ b/src/Metadata/Extractor/YamlResourceExtractor.php @@ -339,6 +339,7 @@ private function buildOperations(array $resource, array $root): ?array 'serialize' => $this->phpize($operation, 'serialize', 'bool'), 'queryParameterValidate' => $this->phpize($operation, 'queryParameterValidate', 'bool'), 'strictQueryParameterValidation' => $this->phpize($operation, 'strictQueryParameterValidation', 'bool'), + 'hideHydraOperation' => $this->phpize($resource, 'hideHydraOperation', 'bool'), 'priority' => $this->phpize($operation, 'priority', 'integer'), 'name' => $this->phpize($operation, 'name', 'string'), 'class' => (string) $class, diff --git a/src/Metadata/Extractor/schema/resources.xsd b/src/Metadata/Extractor/schema/resources.xsd index 28aeda24ffb..b3254bb7790 100644 --- a/src/Metadata/Extractor/schema/resources.xsd +++ b/src/Metadata/Extractor/schema/resources.xsd @@ -516,6 +516,7 @@ + diff --git a/src/Metadata/Get.php b/src/Metadata/Get.php index aecab2d4a0f..c87621fa113 100644 --- a/src/Metadata/Get.php +++ b/src/Metadata/Get.php @@ -99,6 +99,7 @@ public function __construct( ?string $policy = null, array|string|null $middleware = null, ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, array $extraProperties = [], ) { parent::__construct( @@ -179,6 +180,7 @@ class: $class, policy: $policy, middleware: $middleware, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties, ); } diff --git a/src/Metadata/GetCollection.php b/src/Metadata/GetCollection.php index f71a0f844c8..52398d6282c 100644 --- a/src/Metadata/GetCollection.php +++ b/src/Metadata/GetCollection.php @@ -99,6 +99,7 @@ public function __construct( ?string $policy = null, array|string|null $middleware = null, ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, array $extraProperties = [], private ?string $itemUriTemplate = null, ) { @@ -180,6 +181,7 @@ class: $class, policy: $policy, middleware: $middleware, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, stateOptions: $stateOptions, ); } diff --git a/src/Metadata/HttpOperation.php b/src/Metadata/HttpOperation.php index 7537fbd27b1..ab4f4d8f04a 100644 --- a/src/Metadata/HttpOperation.php +++ b/src/Metadata/HttpOperation.php @@ -156,6 +156,7 @@ public function __construct( protected ?array $links = null, protected ?array $errors = null, protected ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, ?string $shortName = null, ?string $class = null, @@ -259,6 +260,7 @@ class: $class, middleware: $middleware, queryParameterValidationEnabled: $queryParameterValidationEnabled, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Metadata.php b/src/Metadata/Metadata.php index b0edc60b598..e5edb91cead 100644 --- a/src/Metadata/Metadata.php +++ b/src/Metadata/Metadata.php @@ -82,6 +82,7 @@ public function __construct( protected array|string|null $middleware = null, protected ?bool $queryParameterValidationEnabled = null, protected ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, protected array $extraProperties = [], ) { if (\is_array($parameters) && $parameters) { @@ -680,4 +681,17 @@ public function withStrictQueryParameterValidation(bool $strictQueryParameterVal return $self; } + + public function getHideHydraOperation(): ?bool + { + return $this->hideHydraOperation; + } + + public function withHideHydraOperation(bool $hideHydraOperation): static + { + $self = clone $this; + $self->hideHydraOperation = $hideHydraOperation; + + return $self; + } } diff --git a/src/Metadata/Operation.php b/src/Metadata/Operation.php index 072954d286c..94d321ff786 100644 --- a/src/Metadata/Operation.php +++ b/src/Metadata/Operation.php @@ -812,6 +812,7 @@ public function __construct( array|string|null $middleware = null, ?bool $queryParameterValidationEnabled = null, protected ?bool $strictQueryParameterValidation = null, + protected ?bool $hideHydraOperation = null, protected array $extraProperties = [], ) { parent::__construct( @@ -858,6 +859,7 @@ class: $class, middleware: $middleware, queryParameterValidationEnabled: $queryParameterValidationEnabled, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties, ); } diff --git a/src/Metadata/Patch.php b/src/Metadata/Patch.php index 6a8351518ca..f3e8774349d 100644 --- a/src/Metadata/Patch.php +++ b/src/Metadata/Patch.php @@ -99,6 +99,7 @@ public function __construct( ?string $policy = null, array|string|null $middleware = null, ?bool $strictQueryParameterValidation = null, + ?bool $hideHydraOperation = null, array $extraProperties = [], ) { parent::__construct( @@ -180,6 +181,7 @@ class: $class, policy: $policy, middleware: $middleware, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Post.php b/src/Metadata/Post.php index 6508da1767c..ebc6f3edbc9 100644 --- a/src/Metadata/Post.php +++ b/src/Metadata/Post.php @@ -101,6 +101,7 @@ public function __construct( array $extraProperties = [], private ?string $itemUriTemplate = null, ?bool $strictQueryParameterValidation = null, + ?bool $hideHydraOperation = null, ) { parent::__construct( method: 'POST', @@ -181,6 +182,7 @@ class: $class, policy: $policy, middleware: $middleware, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Put.php b/src/Metadata/Put.php index 5820e680a8e..2df0c5bf3e0 100644 --- a/src/Metadata/Put.php +++ b/src/Metadata/Put.php @@ -100,6 +100,7 @@ public function __construct( array|string|null $middleware = null, array $extraProperties = [], ?bool $strictQueryParameterValidation = null, + ?bool $hideHydraOperation = null, private ?bool $allowCreate = null, ) { parent::__construct( @@ -181,6 +182,7 @@ class: $class, policy: $policy, middleware: $middleware, strictQueryParameterValidation: $strictQueryParameterValidation, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties ); } diff --git a/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php b/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php index 23980115ed5..749d047e8d8 100644 --- a/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php +++ b/src/Metadata/Tests/Extractor/Adapter/XmlResourceAdapter.php @@ -63,6 +63,7 @@ final class XmlResourceAdapter implements ResourceAdapterInterface 'securityPostValidationMessage', 'queryParameterValidationEnabled', 'strictQueryParameterValidation', + 'hideHydraOperation', 'stateOptions', 'collectDenormalizationErrors', 'links', diff --git a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php index 20d476b83e4..d5105c32123 100644 --- a/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php +++ b/src/Metadata/Tests/Extractor/ResourceMetadataCompatibilityTest.php @@ -97,6 +97,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'securityPostValidationMessage' => 'Sorry, you must the owner of this resource to access it.', 'queryParameterValidationEnabled' => true, 'strictQueryParameterValidation' => false, + 'hideHydraOperation' => false, 'types' => ['someirischema', 'anotheririschema'], 'formats' => [ 'json' => null, @@ -401,6 +402,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase ], 'queryParameterValidationEnabled' => false, 'strictQueryParameterValidation' => false, + 'hideHydraOperation' => false, 'read' => true, 'deserialize' => false, 'validate' => false, @@ -489,6 +491,7 @@ final class ResourceMetadataCompatibilityTest extends TestCase 'controller', 'queryParameterValidationEnabled', 'strictQueryParameterValidation', + 'hideHydraOperation', 'exceptionToStatus', 'types', 'formats', diff --git a/src/Metadata/Tests/Extractor/XmlExtractorTest.php b/src/Metadata/Tests/Extractor/XmlExtractorTest.php index 8f7890c0336..520431d9c8c 100644 --- a/src/Metadata/Tests/Extractor/XmlExtractorTest.php +++ b/src/Metadata/Tests/Extractor/XmlExtractorTest.php @@ -67,6 +67,7 @@ public function testValidXML(): void 'securityPostValidationMessage' => null, 'queryParameterValidationEnabled' => null, 'strictQueryParameterValidation' => null, + 'hideHydraOperation' => null, 'input' => null, 'output' => null, 'types' => null, @@ -138,6 +139,7 @@ public function testValidXML(): void 'securityPostValidationMessage' => null, 'queryParameterValidationEnabled' => null, 'strictQueryParameterValidation' => null, + 'hideHydraOperation' => null, 'input' => null, 'output' => null, 'types' => ['someirischema', 'anotheririschema'], @@ -267,6 +269,7 @@ public function testValidXML(): void 'serialize' => null, 'queryParameterValidate' => null, 'strictQueryParameterValidation' => null, + 'hideHydraOperation' => null, 'collection' => null, 'method' => null, 'priority' => null, @@ -371,6 +374,7 @@ public function testValidXML(): void 'serialize' => null, 'queryParameterValidate' => null, 'strictQueryParameterValidation' => null, + 'hideHydraOperation' => null, 'collection' => null, 'method' => null, 'priority' => null, diff --git a/src/Metadata/Tests/Extractor/YamlExtractorTest.php b/src/Metadata/Tests/Extractor/YamlExtractorTest.php index d4690da9fd0..7ce742ec125 100644 --- a/src/Metadata/Tests/Extractor/YamlExtractorTest.php +++ b/src/Metadata/Tests/Extractor/YamlExtractorTest.php @@ -275,6 +275,7 @@ public function testValidYaml(): void 'securityPostValidationMessage' => null, 'queryParameterValidationEnabled' => null, 'strictQueryParameterValidation' => null, + 'hideHydraOperation' => null, 'input' => null, 'output' => null, 'types' => ['someirischema'], @@ -355,6 +356,7 @@ public function testValidYaml(): void 'securityPostValidationMessage' => null, 'queryParameterValidationEnabled' => null, 'strictQueryParameterValidation' => null, + 'hideHydraOperation' => null, 'input' => null, 'output' => null, 'types' => ['anotheririschema'], diff --git a/tests/Fixtures/TestBundle/ApiResource/HideHydraClass.php b/tests/Fixtures/TestBundle/ApiResource/HideHydraClass.php new file mode 100644 index 00000000000..43e12fd54ab --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/HideHydraClass.php @@ -0,0 +1,33 @@ + + * + * 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\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; + +#[ApiResource( + hideHydraOperation: true, + normalizationContext: ['hydra_prefix' => false], + operations: [ + new Get(), + new GetCollection(), + ] +)] +final class HideHydraClass +{ + public function __construct(public string $id, public string $title) + { + } +} diff --git a/tests/Fixtures/TestBundle/ApiResource/HideHydraOperation.php b/tests/Fixtures/TestBundle/ApiResource/HideHydraOperation.php new file mode 100644 index 00000000000..72bd0811bb2 --- /dev/null +++ b/tests/Fixtures/TestBundle/ApiResource/HideHydraOperation.php @@ -0,0 +1,32 @@ + + * + * 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\Tests\Fixtures\TestBundle\ApiResource; + +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\GetCollection; + +#[ApiResource( + normalizationContext: ['hydra_prefix' => false], + operations: [ + new Get(hideHydraOperation: true), + new GetCollection(hideHydraOperation: true), + ] +)] +final class HideHydraOperation +{ + public function __construct(public string $id, public string $title) + { + } +} diff --git a/tests/Functional/HydraTest.php b/tests/Functional/HydraTest.php new file mode 100644 index 00000000000..f98263f2290 --- /dev/null +++ b/tests/Functional/HydraTest.php @@ -0,0 +1,54 @@ + + * + * 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\Tests\Functional; + +use ApiPlatform\Symfony\Bundle\Test\ApiTestCase; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\HideHydraClass; +use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\HideHydraOperation; +use ApiPlatform\Tests\SetupClassResourcesTrait; + +class HydraTest extends ApiTestCase +{ + use SetupClassResourcesTrait; + + /** + * @return class-string[] + */ + public static function getResources(): array + { + return [HideHydraOperation::class, HideHydraClass::class]; + } + + /** + * The input DTO denormalizes an existing Doctrine entity. + */ + public function testIssue6465(): void + { + $response = self::createClient()->request('GET', 'docs', [ + 'headers' => ['accept' => 'application/ld+json'], + ]); + + foreach ($response->toArray()['hydra:supportedClass'] as $supportedClass) { + $this->assertNotEquals($supportedClass['hydra:title'], 'HideHydraClass'); + if ('HideHydraOperation' === $supportedClass['hydra:title']) { + $this->assertEmpty($supportedClass['hydra:supportedOperation']); + } + } + + $response = self::createClient()->request('GET', 'index', [ + 'headers' => ['accept' => 'application/ld+json'], + ]); + $this->assertArrayNotHasKey('hideHydraOperation', $response->toArray()); + } +}