diff --git a/features/hydra/docs.feature b/features/hydra/docs.feature index 3b5d665a2c6..de6da5aae8d 100644 --- a/features/hydra/docs.feature +++ b/features/hydra/docs.feature @@ -33,9 +33,9 @@ Feature: Documentation support And the JSON node "hydra:description" should contain "Made with love" And the JSON node "hydra:entrypoint" should be equal to "/" # Supported classes - And the Hydra class "The API entrypoint" exists - And the Hydra class "A constraint violation" exists - And the Hydra class "A constraint violation list" exists + And the Hydra class "Entrypoint" exists + And the Hydra class "ConstraintViolation" exists + And the Hydra class "ConstraintViolationList" exists And the Hydra class "CircularReference" exists And the Hydra class "CustomIdentifierDummy" exists And the Hydra class "CustomNormalizedDummy" exists @@ -49,7 +49,6 @@ Feature: Documentation support # Doc And the value of the node "@id" of the Hydra class "Dummy" is "#Dummy" And the value of the node "@type" of the Hydra class "Dummy" is "hydra:Class" - And the value of the node "rdfs:label" of the Hydra class "Dummy" is "Dummy" And the value of the node "hydra:title" of the Hydra class "Dummy" is "Dummy" And the value of the node "hydra:description" of the Hydra class "Dummy" is "Dummy." # Properties @@ -62,7 +61,7 @@ Feature: Documentation support And the value of the node "@type" of the property "name" of the Hydra class "Dummy" is "hydra:SupportedProperty" And the value of the node "hydra:property.@id" of the property "name" of the Hydra class "Dummy" is "https://schema.org/name" And the value of the node "hydra:property.@type" of the property "name" of the Hydra class "Dummy" is "rdf:Property" - And the value of the node "hydra:property.rdfs:label" of the property "name" of the Hydra class "Dummy" is "name" + And the value of the node "hydra:property.label" of the property "name" of the Hydra class "Dummy" is "name" And the value of the node "hydra:property.domain" of the property "name" of the Hydra class "Dummy" is "#Dummy" And the value of the node "hydra:property.range" of the property "name" of the Hydra class "Dummy" is "xmls:string" And the value of the node "hydra:property.range" of the property "relatedDummy" of the Hydra class "Dummy" is "https://schema.org/Product" @@ -74,14 +73,16 @@ Feature: Documentation support And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "hydra:Operation" And the value of the node "@type" of the operation "GET" of the Hydra class "Dummy" contains "schema:FindAction" And the value of the node "hydra:method" of the operation "GET" of the Hydra class "Dummy" is "GET" - And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." - And the value of the node "rdfs:label" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." + And the value of the node "hydra:title" of the operation "GET" of the Hydra class "Dummy" is "getDummy" + And the value of the node "hydra:description" of the operation "GET" of the Hydra class "Dummy" is "Retrieves a Dummy resource." And the value of the node "returns" of the operation "GET" of the Hydra class "Dummy" is "Dummy" - And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource." - And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource." + And the value of the node "hydra:title" of the operation "PUT" of the Hydra class "Dummy" is "putDummy" + And the value of the node "hydra:description" of the operation "PUT" of the Hydra class "Dummy" is "Replaces the Dummy resource." + And the value of the node "hydra:description" of the operation "DELETE" of the Hydra class "Dummy" is "Deletes the Dummy resource." + And the value of the node "hydra:title" of the operation "DELETE" of the Hydra class "Dummy" is "deleteDummy" And the value of the node "returns" of the operation "DELETE" of the Hydra class "Dummy" is "owl:Nothing" # Deprecations And the boolean value of the node "owl:deprecated" of the Hydra class "DeprecatedResource" is true And the boolean value of the node "hydra:property.owl:deprecated" of the property "deprecatedField" of the Hydra class "DeprecatedResource" is true - And the boolean value of the node "owl:deprecated" of the property "The collection of DeprecatedResource resources" of the Hydra class "The API entrypoint" is true + And the boolean value of the node "owl:deprecated" of the property "getDeprecatedResourceCollection" of the Hydra class "Entrypoint" is true And the boolean value of the node "owl:deprecated" of the operation "GET" of the Hydra class "DeprecatedResource" is true diff --git a/features/hydra/error.feature b/features/hydra/error.feature index d43ea714cb5..4e336205c8f 100644 --- a/features/hydra/error.feature +++ b/features/hydra/error.feature @@ -37,9 +37,9 @@ Feature: Error handling And the JSON should be equal to: """ { - "@context": "/contexts/ConstraintViolationList", + "@context": "/contexts/ConstraintViolation", "@id": "/validation_errors/c1051bb4-d103-4f74-8988-acbcafc7fdc3", - "@type": "ConstraintViolationList", + "@type": "ConstraintViolation", "status": 422, "violations": [ { diff --git a/features/main/validation.feature b/features/main/validation.feature index 9dabd131bdd..dcfd05d34b9 100644 --- a/features/main/validation.feature +++ b/features/main/validation.feature @@ -29,8 +29,8 @@ Feature: Using validations groups And the JSON should be a superset of: """ { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", + "@context": "/contexts/ConstraintViolation", + "@type": "ConstraintViolation", "title": "An error occurred", "description": "name: This value should not be null.", "violations": [ @@ -58,8 +58,8 @@ Feature: Using validations groups And the JSON should be a superset of: """ { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", + "@context": "/contexts/ConstraintViolation", + "@type": "ConstraintViolation", "title": "An error occurred", "description": "title: This value should not be null.", "violations": [ @@ -111,8 +111,8 @@ Feature: Using validations groups And the JSON should be a superset of: """ { - "@context": "/contexts/ConstraintViolationList", - "@type": "ConstraintViolationList", + "@context": "/contexts/ConstraintViolation", + "@type": "ConstraintViolation", "hydra:title": "An error occurred", "hydra:description": "baz: This value should be of type string.\nqux: This value should be of type string.\nfoo: This value should be of type bool.\nbar: This value should be of type int.\nuuid: This value should be of type uuid.\nrelatedDummy: This value should be of type array|string.\nrelatedDummies: This value should be of type array.", "violations": [ diff --git a/src/Hydra/Serializer/DocumentationNormalizer.php b/src/Hydra/Serializer/DocumentationNormalizer.php index 55a41b915a2..2903eb55571 100644 --- a/src/Hydra/Serializer/DocumentationNormalizer.php +++ b/src/Hydra/Serializer/DocumentationNormalizer.php @@ -29,7 +29,6 @@ use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Metadata\ResourceClassResolverInterface; use ApiPlatform\Metadata\UrlGeneratorInterface; -use ApiPlatform\Validator\Exception\ValidationException; use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\NameConverter\NameConverterInterface; use Symfony\Component\Serializer\Normalizer\AbstractNormalizer; @@ -71,17 +70,13 @@ public function normalize(mixed $object, ?string $format = null, array $context $resourceMetadataCollection = $this->resourceMetadataFactory->create($resourceClass); $resourceMetadata = $resourceMetadataCollection[0]; - if ($resourceMetadata instanceof ErrorResource && ValidationException::class === $resourceMetadata->getClass()) { - continue; - } - if (true === $resourceMetadata->getHideHydraOperation()) { continue; } $shortName = $resourceMetadata->getShortName(); - $prefixedShortName = $resourceMetadata->getTypes()[0] ?? "#$shortName"; + $this->populateEntrypointProperties($resourceMetadata, $shortName, $prefixedShortName, $entrypointProperties, $hydraPrefix, $resourceMetadataCollection); $classes[] = $this->getClass($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix, $resourceMetadataCollection); } @@ -105,8 +100,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str '@id' => \sprintf('#Entrypoint/%s', lcfirst($shortName)), '@type' => $hydraPrefix.'Link', 'domain' => '#Entrypoint', - 'rdfs:label' => "The collection of $shortName resources", - 'rdfs:range' => [ + 'owl:maxCardinality' => 1, + 'range' => [ ['@id' => $hydraPrefix.'Collection'], [ 'owl:equivalentClass' => [ @@ -117,7 +112,8 @@ private function populateEntrypointProperties(ApiResource $resourceMetadata, str ], $hydraPrefix.'supportedOperation' => $hydraCollectionOperations, ], - $hydraPrefix.'title' => "The collection of $shortName resources", + $hydraPrefix.'title' => "get{$shortName}Collection", + $hydraPrefix.'description' => "The collection of $shortName resources", $hydraPrefix.'readable' => true, $hydraPrefix.'writeable' => false, ]; @@ -140,7 +136,6 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata, $class = [ '@id' => $prefixedShortName, '@type' => $hydraPrefix.'Class', - 'rdfs:label' => $shortName, $hydraPrefix.'title' => $shortName, $hydraPrefix.'supportedProperty' => $this->getHydraProperties($resourceClass, $resourceMetadata, $shortName, $prefixedShortName, $context, $hydraPrefix), $hydraPrefix.'supportedOperation' => $this->getHydraOperations(false, $resourceMetadataCollection, $hydraPrefix), @@ -150,6 +145,10 @@ private function getClass(string $resourceClass, ApiResource $resourceMetadata, $class[$hydraPrefix.'description'] = $description; } + if ($resourceMetadata instanceof ErrorResource) { + $class['subClassOf'] = 'Error'; + } + if ($isDeprecated) { $class['owl:deprecated'] = true; } @@ -232,6 +231,10 @@ private function getHydraProperties(string $resourceClass, ApiResource $resource $propertyName = $this->nameConverter->normalize($propertyName, $class, self::FORMAT, $context); } + if (false === $propertyMetadata->getHydra()) { + continue; + } + $properties[] = $this->getProperty($propertyMetadata, $propertyName, $prefixedShortName, $shortName, $hydraPrefix); } } @@ -254,6 +257,7 @@ private function getHydraOperations(bool $collection, ?ResourceMetadataCollectio if (('POST' === $operation->getMethod() || $operation instanceof CollectionOperationInterface) !== $collection) { continue; } + $hydraOperations[] = $this->getHydraOperation($operation, $operation->getShortName(), $hydraPrefix); } } @@ -283,49 +287,57 @@ private function getHydraOperation(HttpOperation $operation, string $prefixedSho if ('GET' === $method && $operation instanceof CollectionOperationInterface) { $hydraOperation += [ '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'], - $hydraPrefix.'title' => "Retrieves the collection of $shortName resources.", + $hydraPrefix.'description' => "Retrieves the collection of $shortName resources.", 'returns' => null === $outputClass ? 'owl:Nothing' : $hydraPrefix.'Collection', ]; } elseif ('GET' === $method) { $hydraOperation += [ '@type' => [$hydraPrefix.'Operation', 'schema:FindAction'], - $hydraPrefix.'title' => "Retrieves a $shortName resource.", + $hydraPrefix.'description' => "Retrieves a $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, ]; } elseif ('PATCH' === $method) { $hydraOperation += [ '@type' => $hydraPrefix.'Operation', - $hydraPrefix.'title' => "Updates the $shortName resource.", + $hydraPrefix.'description' => "Updates the $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, ]; + + if (null !== $inputClass) { + $possibleValue = []; + foreach ($operation->getInputFormats() as $mimeTypes) { + foreach ($mimeTypes as $mimeType) { + $possibleValue[] = $mimeType; + } + } + + $hydraOperation['expectsHeader'] = [['headerName' => 'Content-Type', 'possibleValue' => $possibleValue]]; + } } elseif ('POST' === $method) { $hydraOperation += [ '@type' => [$hydraPrefix.'Operation', 'schema:CreateAction'], - $hydraPrefix.'title' => "Creates a $shortName resource.", + $hydraPrefix.'description' => "Creates a $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, ]; } elseif ('PUT' === $method) { $hydraOperation += [ '@type' => [$hydraPrefix.'Operation', 'schema:ReplaceAction'], - $hydraPrefix.'title' => "Replaces the $shortName resource.", + $hydraPrefix.'description' => "Replaces the $shortName resource.", 'returns' => null === $outputClass ? 'owl:Nothing' : $prefixedShortName, 'expects' => null === $inputClass ? 'owl:Nothing' : $prefixedShortName, ]; } elseif ('DELETE' === $method) { $hydraOperation += [ '@type' => [$hydraPrefix.'Operation', 'schema:DeleteAction'], - $hydraPrefix.'title' => "Deletes the $shortName resource.", + $hydraPrefix.'description' => "Deletes the $shortName resource.", 'returns' => 'owl:Nothing', ]; } - $hydraOperation[$hydraPrefix.'method'] ?? $hydraOperation[$hydraPrefix.'method'] = $method; - - if (!isset($hydraOperation['rdfs:label']) && isset($hydraOperation[$hydraPrefix.'title'])) { - $hydraOperation['rdfs:label'] = $hydraOperation[$hydraPrefix.'title']; - } + $hydraOperation[$hydraPrefix.'method'] ??= $method; + $hydraOperation[$hydraPrefix.'title'] ??= strtolower($method).$shortName.($operation instanceof CollectionOperationInterface ? 'Collection' : ''); ksort($hydraOperation); @@ -434,29 +446,30 @@ private function getClasses(array $entrypointProperties, array $classes, string $classes[] = [ '@id' => '#Entrypoint', '@type' => $hydraPrefix.'Class', - $hydraPrefix.'title' => 'The API entrypoint', + $hydraPrefix.'title' => 'Entrypoint', $hydraPrefix.'supportedProperty' => $entrypointProperties, $hydraPrefix.'supportedOperation' => [ '@type' => $hydraPrefix.'Operation', $hydraPrefix.'method' => 'GET', - 'rdfs:label' => 'The API entrypoint.', - 'returns' => 'EntryPoint', + $hydraPrefix.'title' => 'index', + $hydraPrefix.'description' => 'The API Entrypoint.', + $hydraPrefix.'returns' => 'Entrypoint', ], ]; - // Constraint violation $classes[] = [ - '@id' => '#ConstraintViolation', + '@id' => '#ConstraintViolationList', '@type' => $hydraPrefix.'Class', - $hydraPrefix.'title' => 'A constraint violation', + $hydraPrefix.'title' => 'ConstraintViolationList', + $hydraPrefix.'description' => 'A constraint violation List.', $hydraPrefix.'supportedProperty' => [ [ '@type' => $hydraPrefix.'SupportedProperty', $hydraPrefix.'property' => [ - '@id' => '#ConstraintViolation/propertyPath', + '@id' => '#ConstraintViolationList/propertyPath', '@type' => 'rdf:Property', 'rdfs:label' => 'propertyPath', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], $hydraPrefix.'title' => 'propertyPath', @@ -467,10 +480,10 @@ private function getClasses(array $entrypointProperties, array $classes, string [ '@type' => $hydraPrefix.'SupportedProperty', $hydraPrefix.'property' => [ - '@id' => '#ConstraintViolation/message', + '@id' => '#ConstraintViolationList/message', '@type' => 'rdf:Property', 'rdfs:label' => 'message', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], $hydraPrefix.'title' => 'message', @@ -481,30 +494,6 @@ private function getClasses(array $entrypointProperties, array $classes, string ], ]; - // Constraint violation list - $classes[] = [ - '@id' => '#ConstraintViolationList', - '@type' => $hydraPrefix.'Class', - 'subClassOf' => $hydraPrefix.'Error', - $hydraPrefix.'title' => 'A constraint violation list', - $hydraPrefix.'supportedProperty' => [ - [ - '@type' => $hydraPrefix.'SupportedProperty', - $hydraPrefix.'property' => [ - '@id' => '#ConstraintViolationList/violations', - '@type' => 'rdf:Property', - 'rdfs:label' => 'violations', - 'domain' => '#ConstraintViolationList', - 'range' => '#ConstraintViolation', - ], - $hydraPrefix.'title' => 'violations', - $hydraPrefix.'description' => 'The violations', - $hydraPrefix.'readable' => true, - $hydraPrefix.'writeable' => false, - ], - ], - ]; - return $classes; } @@ -524,8 +513,8 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName $propertyData = ($propertyMetadata->getJsonldContext()[$hydraPrefix.'property'] ?? []) + [ '@id' => $iri, '@type' => false === $propertyMetadata->isReadableLink() ? $hydraPrefix.'Link' : 'rdf:Property', - 'rdfs:label' => $propertyName, 'domain' => $prefixedShortName, + 'label' => $propertyName, ]; if (!isset($propertyData['owl:deprecated']) && $propertyMetadata->getDeprecationReason()) { @@ -544,7 +533,7 @@ private function getProperty(ApiProperty $propertyMetadata, string $propertyName '@type' => $hydraPrefix.'SupportedProperty', $hydraPrefix.'property' => $propertyData, $hydraPrefix.'title' => $propertyName, - $hydraPrefix.'required' => $propertyMetadata->isRequired(), + $hydraPrefix.'required' => $propertyMetadata->isRequired() ?? false, $hydraPrefix.'readable' => $propertyMetadata->isReadable(), $hydraPrefix.'writeable' => $propertyMetadata->isWritable() || $propertyMetadata->isInitializable(), ]; diff --git a/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php index f82f796a7fe..142acf2d323 100644 --- a/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php +++ b/src/Hydra/Tests/Serializer/DocumentationNormalizerTest.php @@ -139,7 +139,6 @@ private function doTestNormalize($resourceMetadataFactory = null): void [ '@id' => '#dummy', '@type' => 'hydra:Class', - 'rdfs:label' => 'dummy', 'hydra:title' => 'dummy', 'hydra:description' => 'dummy', 'hydra:supportedProperty' => [ @@ -148,7 +147,7 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:property' => [ '@id' => '#dummy/name', '@type' => 'rdf:Property', - 'rdfs:label' => 'name', + 'label' => 'name', 'domain' => '#dummy', 'range' => 'xmls:string', ], @@ -163,7 +162,7 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:property' => [ '@id' => '#dummy/description', '@type' => 'rdf:Property', - 'rdfs:label' => 'description', + 'label' => 'description', 'domain' => '#dummy', 'range' => '@id', ], @@ -178,7 +177,7 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:property' => [ '@id' => '#dummy/name_converted', '@type' => 'rdf:Property', - 'rdfs:label' => 'name_converted', + 'label' => 'name_converted', 'domain' => '#dummy', 'range' => 'xmls:string', ], @@ -193,7 +192,7 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:property' => [ '@id' => '#dummy/relatedDummy', '@type' => 'rdf:Property', - 'rdfs:label' => 'relatedDummy', + 'label' => 'relatedDummy', 'domain' => '#dummy', 'range' => '#relatedDummy', ], @@ -208,7 +207,7 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:property' => [ '@id' => 'https://schema.org/Dummy', '@type' => 'rdf:Property', - 'rdfs:label' => 'iri', + 'label' => 'iri', 'domain' => '#dummy', ], 'hydra:title' => 'iri', @@ -222,23 +221,23 @@ private function doTestNormalize($resourceMetadataFactory = null): void '@type' => ['hydra:Operation', 'schema:FindAction'], 'hydra:method' => 'GET', 'hydra:title' => 'foobar', - 'rdfs:label' => 'foobar', 'returns' => 'dummy', 'hydra:foo' => 'bar', + 'hydra:description' => 'Retrieves a dummy resource.', ], [ '@type' => ['hydra:Operation', 'schema:ReplaceAction'], 'expects' => 'dummy', 'hydra:method' => 'PUT', - 'hydra:title' => 'Replaces the dummy resource.', - 'rdfs:label' => 'Replaces the dummy resource.', + 'hydra:title' => 'putdummy', + 'hydra:description' => 'Replaces the dummy resource.', 'returns' => 'dummy', ], [ '@type' => ['hydra:Operation', 'schema:FindAction'], 'hydra:method' => 'GET', - 'hydra:title' => 'Retrieves a relatedDummy resource.', - 'rdfs:label' => 'Retrieves a relatedDummy resource.', + 'hydra:title' => 'getrelatedDummy', + 'hydra:description' => 'Retrieves a relatedDummy resource.', 'returns' => 'relatedDummy', ], ], @@ -246,16 +245,15 @@ private function doTestNormalize($resourceMetadataFactory = null): void [ '@id' => '#Entrypoint', '@type' => 'hydra:Class', - 'hydra:title' => 'The API entrypoint', + 'hydra:title' => 'Entrypoint', 'hydra:supportedProperty' => [ [ '@type' => 'hydra:SupportedProperty', 'hydra:property' => [ '@id' => '#Entrypoint/dummy', '@type' => 'hydra:Link', - 'rdfs:label' => 'The collection of dummy resources', 'domain' => '#Entrypoint', - 'rdfs:range' => [ + 'range' => [ ['@id' => 'hydra:Collection'], [ 'owl:equivalentClass' => [ @@ -264,25 +262,27 @@ private function doTestNormalize($resourceMetadataFactory = null): void ], ], ], + 'owl:maxCardinality' => 1, 'hydra:supportedOperation' => [ [ '@type' => ['hydra:Operation', 'schema:FindAction'], 'hydra:method' => 'GET', - 'hydra:title' => 'Retrieves the collection of dummy resources.', - 'rdfs:label' => 'Retrieves the collection of dummy resources.', + 'hydra:title' => 'getdummyCollection', + 'hydra:description' => 'Retrieves the collection of dummy resources.', 'returns' => 'hydra:Collection', ], [ '@type' => ['hydra:Operation', 'schema:CreateAction'], 'expects' => 'dummy', 'hydra:method' => 'POST', - 'hydra:title' => 'Creates a dummy resource.', - 'rdfs:label' => 'Creates a dummy resource.', + 'hydra:title' => 'postdummy', + 'hydra:description' => 'Creates a dummy resource.', 'returns' => 'dummy', ], ], ], - 'hydra:title' => 'The collection of dummy resources', + 'hydra:title' => 'getdummyCollection', + 'hydra:description' => 'The collection of dummy resources', 'hydra:readable' => true, 'hydra:writeable' => false, ], @@ -290,22 +290,24 @@ private function doTestNormalize($resourceMetadataFactory = null): void 'hydra:supportedOperation' => [ '@type' => 'hydra:Operation', 'hydra:method' => 'GET', - 'rdfs:label' => 'The API entrypoint.', - 'returns' => 'EntryPoint', + 'hydra:title' => 'index', + 'hydra:description' => 'The API Entrypoint.', + 'hydra:returns' => 'Entrypoint', ], ], [ - '@id' => '#ConstraintViolation', + '@id' => '#ConstraintViolationList', '@type' => 'hydra:Class', - 'hydra:title' => 'A constraint violation', + 'hydra:title' => 'ConstraintViolationList', + 'hydra:description' => 'A constraint violation List.', 'hydra:supportedProperty' => [ [ '@type' => 'hydra:SupportedProperty', 'hydra:property' => [ - '@id' => '#ConstraintViolation/propertyPath', + '@id' => '#ConstraintViolationList/propertyPath', '@type' => 'rdf:Property', 'rdfs:label' => 'propertyPath', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], 'hydra:title' => 'propertyPath', @@ -316,10 +318,10 @@ private function doTestNormalize($resourceMetadataFactory = null): void [ '@type' => 'hydra:SupportedProperty', 'hydra:property' => [ - '@id' => '#ConstraintViolation/message', + '@id' => '#ConstraintViolationList/message', '@type' => 'rdf:Property', 'rdfs:label' => 'message', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], 'hydra:title' => 'message', @@ -329,28 +331,6 @@ private function doTestNormalize($resourceMetadataFactory = null): void ], ], ], - [ - '@id' => '#ConstraintViolationList', - '@type' => 'hydra:Class', - 'subClassOf' => 'hydra:Error', - 'hydra:title' => 'A constraint violation list', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolationList/violations', - '@type' => 'rdf:Property', - 'rdfs:label' => 'violations', - 'domain' => '#ConstraintViolationList', - 'range' => '#ConstraintViolation', - ], - 'hydra:title' => 'violations', - 'hydra:description' => 'The violations', - 'hydra:readable' => true, - 'hydra:writeable' => false, - ], - ], - ], ], 'hydra:entrypoint' => '/', ]; @@ -407,252 +387,99 @@ public function testNormalizeInputOutputClass(): void ); $expected = [ - '@context' => [ - HYDRA_CONTEXT, + '@id' => '#dummy', + '@type' => 'hydra:Class', + 'hydra:title' => 'dummy', + 'hydra:supportedProperty' => [ [ - '@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', + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/a', + '@type' => 'rdf:Property', + 'label' => 'a', + 'domain' => '#dummy', + 'range' => 'xmls:string', ], + 'hydra:title' => 'a', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'a', ], - ], - '@id' => '/doc', - '@type' => 'hydra:ApiDocumentation', - 'hydra:title' => 'Test Api', - 'hydra:description' => 'test ApiGerard', - 'hydra:entrypoint' => '/', - 'hydra:supportedClass' => [ [ - '@id' => '#dummy', - '@type' => 'hydra:Class', - 'rdfs:label' => 'dummy', - 'hydra:title' => 'dummy', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#dummy/a', - '@type' => 'rdf:Property', - 'rdfs:label' => 'a', - 'domain' => '#dummy', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'a', - 'hydra:required' => false, - 'hydra:readable' => true, - 'hydra:writeable' => true, - 'hydra:description' => 'a', - ], - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#dummy/b', - '@type' => 'rdf:Property', - 'rdfs:label' => 'b', - 'domain' => '#dummy', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'b', - 'hydra:required' => false, - 'hydra:readable' => true, - 'hydra:writeable' => true, - 'hydra:description' => 'b', - ], - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#dummy/c', - '@type' => 'rdf:Property', - 'rdfs:label' => 'c', - 'domain' => '#dummy', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'c', - 'hydra:required' => false, - 'hydra:readable' => true, - 'hydra:writeable' => true, - 'hydra:description' => 'c', - ], - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#dummy/d', - '@type' => 'rdf:Property', - 'rdfs:label' => 'd', - 'domain' => '#dummy', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'd', - 'hydra:required' => false, - 'hydra:readable' => true, - 'hydra:writeable' => true, - 'hydra:description' => 'd', - ], - ], - 'hydra:supportedOperation' => [ - [ - '@type' => [ - 'hydra:Operation', - 'schema:FindAction', - ], - 'hydra:method' => 'GET', - 'hydra:title' => 'Retrieves a dummy resource.', - 'rdfs:label' => 'Retrieves a dummy resource.', - 'returns' => 'dummy', - ], - [ - '@type' => [ - 'hydra:Operation', - 'schema:ReplaceAction', - ], - 'expects' => 'owl:Nothing', - 'hydra:method' => 'PUT', - 'hydra:title' => 'Replaces the dummy resource.', - 'rdfs:label' => 'Replaces the dummy resource.', - 'returns' => 'dummy', - ], + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/b', + '@type' => 'rdf:Property', + 'label' => 'b', + 'domain' => '#dummy', + 'range' => 'xmls:string', ], - 'hydra:description' => 'dummy', + 'hydra:title' => 'b', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'b', ], [ - '@id' => '#Entrypoint', - '@type' => 'hydra:Class', - 'hydra:title' => 'The API entrypoint', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#Entrypoint/dummy', - '@type' => 'hydra:Link', - 'domain' => '#Entrypoint', - 'rdfs:label' => 'The collection of dummy resources', - 'rdfs:range' => [ - [ - '@id' => 'hydra:Collection', - ], - [ - 'owl:equivalentClass' => [ - 'owl:onProperty' => [ - '@id' => 'hydra:member', - ], - 'owl:allValuesFrom' => [ - '@id' => '#dummy', - ], - ], - ], - ], - 'hydra:supportedOperation' => [ - [ - '@type' => [ - 'hydra:Operation', - 'schema:FindAction', - ], - 'hydra:method' => 'GET', - 'hydra:title' => 'Retrieves the collection of dummy resources.', - 'rdfs:label' => 'Retrieves the collection of dummy resources.', - 'returns' => 'hydra:Collection', - ], - [ - '@type' => [ - 'hydra:Operation', - 'schema:CreateAction', - ], - 'expects' => 'dummy', - 'hydra:method' => 'POST', - 'hydra:title' => 'Creates a dummy resource.', - 'rdfs:label' => 'Creates a dummy resource.', - 'returns' => 'owl:Nothing', - ], - ], - ], - 'hydra:title' => 'The collection of dummy resources', - 'hydra:readable' => true, - 'hydra:writeable' => false, - ], + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/c', + '@type' => 'rdf:Property', + 'label' => 'c', + 'domain' => '#dummy', + 'range' => 'xmls:string', ], - 'hydra:supportedOperation' => [ - '@type' => 'hydra:Operation', - 'hydra:method' => 'GET', - 'rdfs:label' => 'The API entrypoint.', - 'returns' => 'EntryPoint', + 'hydra:title' => 'c', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'c', + ], + [ + '@type' => 'hydra:SupportedProperty', + 'hydra:property' => [ + '@id' => '#dummy/d', + '@type' => 'rdf:Property', + 'label' => 'd', + 'domain' => '#dummy', + 'range' => 'xmls:string', ], + 'hydra:title' => 'd', + 'hydra:required' => false, + 'hydra:readable' => true, + 'hydra:writeable' => true, + 'hydra:description' => 'd', ], - 2 => [ - '@id' => '#ConstraintViolation', - '@type' => 'hydra:Class', - 'hydra:title' => 'A constraint violation', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolation/propertyPath', - '@type' => 'rdf:Property', - 'rdfs:label' => 'propertyPath', - 'domain' => '#ConstraintViolation', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'propertyPath', - 'hydra:description' => 'The property path of the violation', - 'hydra:readable' => true, - 'hydra:writeable' => false, - ], - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolation/message', - '@type' => 'rdf:Property', - 'rdfs:label' => 'message', - 'domain' => '#ConstraintViolation', - 'range' => 'xmls:string', - ], - 'hydra:title' => 'message', - 'hydra:description' => 'The message associated with the violation', - 'hydra:readable' => true, - 'hydra:writeable' => false, - ], + ], + 'hydra:supportedOperation' => [ + [ + '@type' => [ + 'hydra:Operation', + 'schema:FindAction', ], + 'hydra:method' => 'GET', + 'hydra:title' => 'getdummy', + 'hydra:description' => 'Retrieves a dummy resource.', + 'returns' => 'dummy', ], [ - '@id' => '#ConstraintViolationList', - '@type' => 'hydra:Class', - 'subClassOf' => 'hydra:Error', - 'hydra:title' => 'A constraint violation list', - 'hydra:supportedProperty' => [ - [ - '@type' => 'hydra:SupportedProperty', - 'hydra:property' => [ - '@id' => '#ConstraintViolationList/violations', - '@type' => 'rdf:Property', - 'rdfs:label' => 'violations', - 'domain' => '#ConstraintViolationList', - 'range' => '#ConstraintViolation', - ], - 'hydra:title' => 'violations', - 'hydra:description' => 'The violations', - 'hydra:readable' => true, - 'hydra:writeable' => false, - ], + '@type' => [ + 'hydra:Operation', + 'schema:ReplaceAction', ], + 'expects' => 'owl:Nothing', + 'hydra:method' => 'PUT', + 'hydra:title' => 'putdummy', + 'hydra:description' => 'Replaces the dummy resource.', + 'returns' => 'dummy', ], ], + 'hydra:description' => 'dummy', ]; - $this->assertEquals($expected, $documentationNormalizer->normalize($documentation)); + $doc = $documentationNormalizer->normalize($documentation); + $this->assertEquals($expected, $doc['hydra:supportedClass'][0]); } public function testHasHydraContext(): void @@ -705,7 +532,7 @@ public function testHasHydraContext(): void $this->assertEquals([ '@id' => '#dummy/name', '@type' => 'https://schema.org/Enumeration', - 'rdfs:label' => 'name', + 'label' => 'name', 'domain' => '#dummy', 'range' => 'xmls:string', ], $documentationNormalizer->normalize($documentation)['hydra:supportedClass'][0]['hydra:supportedProperty'][0]['hydra:property']); @@ -799,7 +626,6 @@ public function testNormalizeWithoutPrefix(): void [ '@id' => '#dummy', '@type' => 'Class', - 'rdfs:label' => 'dummy', 'title' => 'dummy', 'description' => 'dummy', 'supportedProperty' => [ @@ -808,7 +634,7 @@ public function testNormalizeWithoutPrefix(): void 'property' => [ '@id' => '#dummy/name', '@type' => 'rdf:Property', - 'rdfs:label' => 'name', + 'label' => 'name', 'domain' => '#dummy', 'range' => 'xmls:string', ], @@ -823,7 +649,7 @@ public function testNormalizeWithoutPrefix(): void 'property' => [ '@id' => '#dummy/description', '@type' => 'rdf:Property', - 'rdfs:label' => 'description', + 'label' => 'description', 'domain' => '#dummy', 'range' => '@id', ], @@ -838,7 +664,7 @@ public function testNormalizeWithoutPrefix(): void 'property' => [ '@id' => '#dummy/name_converted', '@type' => 'rdf:Property', - 'rdfs:label' => 'name_converted', + 'label' => 'name_converted', 'domain' => '#dummy', 'range' => 'xmls:string', ], @@ -853,7 +679,7 @@ public function testNormalizeWithoutPrefix(): void 'property' => [ '@id' => '#dummy/relatedDummy', '@type' => 'rdf:Property', - 'rdfs:label' => 'relatedDummy', + 'label' => 'relatedDummy', 'domain' => '#dummy', 'range' => '#relatedDummy', ], @@ -868,7 +694,7 @@ public function testNormalizeWithoutPrefix(): void 'property' => [ '@id' => 'https://schema.org/Dummy', '@type' => 'rdf:Property', - 'rdfs:label' => 'iri', + 'label' => 'iri', 'domain' => '#dummy', ], 'title' => 'iri', @@ -882,23 +708,23 @@ public function testNormalizeWithoutPrefix(): void '@type' => ['Operation', 'schema:FindAction'], 'method' => 'GET', 'title' => 'foobar', - 'rdfs:label' => 'foobar', 'returns' => 'dummy', 'foo' => 'bar', + 'description' => 'Retrieves a dummy resource.', ], [ '@type' => ['Operation', 'schema:ReplaceAction'], 'expects' => 'dummy', 'method' => 'PUT', - 'title' => 'Replaces the dummy resource.', - 'rdfs:label' => 'Replaces the dummy resource.', + 'title' => 'putdummy', + 'description' => 'Replaces the dummy resource.', 'returns' => 'dummy', ], [ '@type' => ['Operation', 'schema:FindAction'], 'method' => 'GET', - 'title' => 'Retrieves a relatedDummy resource.', - 'rdfs:label' => 'Retrieves a relatedDummy resource.', + 'title' => 'getrelatedDummy', + 'description' => 'Retrieves a relatedDummy resource.', 'returns' => 'relatedDummy', ], ], @@ -906,16 +732,15 @@ public function testNormalizeWithoutPrefix(): void [ '@id' => '#Entrypoint', '@type' => 'Class', - 'title' => 'The API entrypoint', + 'title' => 'Entrypoint', 'supportedProperty' => [ [ '@type' => 'SupportedProperty', 'property' => [ '@id' => '#Entrypoint/dummy', '@type' => 'Link', - 'rdfs:label' => 'The collection of dummy resources', 'domain' => '#Entrypoint', - 'rdfs:range' => [ + 'range' => [ ['@id' => 'Collection'], [ 'owl:equivalentClass' => [ @@ -924,25 +749,27 @@ public function testNormalizeWithoutPrefix(): void ], ], ], + 'owl:maxCardinality' => 1, 'supportedOperation' => [ [ '@type' => ['Operation', 'schema:FindAction'], 'method' => 'GET', - 'title' => 'Retrieves the collection of dummy resources.', - 'rdfs:label' => 'Retrieves the collection of dummy resources.', + 'title' => 'getdummyCollection', + 'description' => 'Retrieves the collection of dummy resources.', 'returns' => 'Collection', ], [ '@type' => ['Operation', 'schema:CreateAction'], 'expects' => 'dummy', 'method' => 'POST', - 'title' => 'Creates a dummy resource.', - 'rdfs:label' => 'Creates a dummy resource.', + 'title' => 'postdummy', + 'description' => 'Creates a dummy resource.', 'returns' => 'dummy', ], ], ], - 'title' => 'The collection of dummy resources', + 'title' => 'getdummyCollection', + 'description' => 'The collection of dummy resources', 'readable' => true, 'writeable' => false, ], @@ -950,22 +777,24 @@ public function testNormalizeWithoutPrefix(): void 'supportedOperation' => [ '@type' => 'Operation', 'method' => 'GET', - 'rdfs:label' => 'The API entrypoint.', - 'returns' => 'EntryPoint', + 'title' => 'index', + 'description' => 'The API Entrypoint.', + 'returns' => 'Entrypoint', ], ], [ - '@id' => '#ConstraintViolation', + '@id' => '#ConstraintViolationList', '@type' => 'Class', - 'title' => 'A constraint violation', + 'title' => 'ConstraintViolationList', + 'description' => 'A constraint violation List.', 'supportedProperty' => [ [ '@type' => 'SupportedProperty', 'property' => [ - '@id' => '#ConstraintViolation/propertyPath', + '@id' => '#ConstraintViolationList/propertyPath', '@type' => 'rdf:Property', 'rdfs:label' => 'propertyPath', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], 'title' => 'propertyPath', @@ -976,10 +805,10 @@ public function testNormalizeWithoutPrefix(): void [ '@type' => 'SupportedProperty', 'property' => [ - '@id' => '#ConstraintViolation/message', + '@id' => '#ConstraintViolationList/message', '@type' => 'rdf:Property', 'rdfs:label' => 'message', - 'domain' => '#ConstraintViolation', + 'domain' => '#ConstraintViolationList', 'range' => 'xmls:string', ], 'title' => 'message', @@ -989,28 +818,6 @@ public function testNormalizeWithoutPrefix(): void ], ], ], - [ - '@id' => '#ConstraintViolationList', - '@type' => 'Class', - 'subClassOf' => 'Error', - 'title' => 'A constraint violation list', - 'supportedProperty' => [ - [ - '@type' => 'SupportedProperty', - 'property' => [ - '@id' => '#ConstraintViolationList/violations', - '@type' => 'rdf:Property', - 'rdfs:label' => 'violations', - 'domain' => '#ConstraintViolationList', - 'range' => '#ConstraintViolation', - ], - 'title' => 'violations', - 'description' => 'The violations', - 'readable' => true, - 'writeable' => false, - ], - ], - ], ], 'entrypoint' => '/', ]; diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index 1839f220b2b..879465d0369 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -26,7 +26,7 @@ * * @author Kévin Dunglas */ -#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS)] +#[\Attribute(\Attribute::TARGET_PROPERTY | \Attribute::TARGET_METHOD | \Attribute::TARGET_PARAMETER | \Attribute::TARGET_CLASS_CONSTANT | \Attribute::TARGET_CLASS | \Attribute::IS_REPEATABLE)] final class ApiProperty { private ?array $types; @@ -216,6 +216,10 @@ public function __construct( private ?string $property = null, private ?string $policy = null, array|Context|Groups|Ignore|SerializedName|SerializedPath|MaxDepth|null $serialize = null, + /** + * Whether to document this property as a hydra:supportedProperty. + */ + private ?bool $hydra = null, private array $extraProperties = [], ) { $this->types = \is_string($types) ? (array) $types : $types; @@ -517,7 +521,7 @@ public function withSchema(array $schema = []): self return $self; } - public function withInitializable(?bool $initializable): self + public function withInitializable(bool $initializable): self { $self = clone $this; $self->initializable = $initializable; @@ -626,4 +630,17 @@ public function withSerialize(array|Context|Groups|Ignore|SerializedName|Seriali return $self; } + + public function getHydra(): ?bool + { + return $this->hydra; + } + + public function withHydra(bool $hydra): static + { + $self = clone $this; + $self->hydra = $hydra; + + return $self; + } } diff --git a/src/Metadata/Error.php b/src/Metadata/Error.php index 84a652c0f1e..43325a60964 100644 --- a/src/Metadata/Error.php +++ b/src/Metadata/Error.php @@ -94,6 +94,7 @@ public function __construct( $provider = null, $processor = null, ?OptionsInterface $stateOptions = null, + ?bool $hideHydraOperation = null, array $extraProperties = [], ) { parent::__construct( @@ -169,6 +170,7 @@ class: $class, provider: $provider, processor: $processor, stateOptions: $stateOptions, + hideHydraOperation: $hideHydraOperation, extraProperties: $extraProperties, ); } diff --git a/src/Metadata/Extractor/schema/properties.xsd b/src/Metadata/Extractor/schema/properties.xsd index 06194f10651..5664433cf64 100644 --- a/src/Metadata/Extractor/schema/properties.xsd +++ b/src/Metadata/Extractor/schema/properties.xsd @@ -46,6 +46,7 @@ + diff --git a/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php b/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php index 5af03c71190..cdfdcd245fd 100644 --- a/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php +++ b/src/Metadata/Tests/Extractor/Adapter/XmlPropertyAdapter.php @@ -44,6 +44,7 @@ final class XmlPropertyAdapter implements PropertyAdapterInterface 'iris', 'genId', 'uriTemplate', + 'hydra', 'property', ]; diff --git a/src/Metadata/Tests/Extractor/Adapter/properties.xml b/src/Metadata/Tests/Extractor/Adapter/properties.xml index e2ae0f27fd8..1bd37ee8b11 100644 --- a/src/Metadata/Tests/Extractor/Adapter/properties.xml +++ b/src/Metadata/Tests/Extractor/Adapter/properties.xml @@ -1,3 +1,3 @@ -bazbaripsumsomeirischemaanotheririschemastringhttps://schema.org/Thinghttps://schema.org/totalPriceLorem ipsum dolor sit amet1 +bazbaripsumsomeirischemaanotheririschemastringhttps://schema.org/Thinghttps://schema.org/totalPriceLorem ipsum dolor sit amet1 diff --git a/src/Metadata/Tests/Extractor/Adapter/properties.yaml b/src/Metadata/Tests/Extractor/Adapter/properties.yaml index c3b4cb2e175..9ec0d1458db 100644 --- a/src/Metadata/Tests/Extractor/Adapter/properties.yaml +++ b/src/Metadata/Tests/Extractor/Adapter/properties.yaml @@ -40,3 +40,4 @@ properties: genId: true uriTemplate: /sub-resource-get-collection property: test + hydra: false diff --git a/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php b/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php index 55ebdb5d919..0119918b519 100644 --- a/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php +++ b/src/Metadata/Tests/Extractor/PropertyMetadataCompatibilityTest.php @@ -76,6 +76,7 @@ final class PropertyMetadataCompatibilityTest extends TestCase 'genId' => true, 'uriTemplate' => '/sub-resource-get-collection', 'property' => 'test', + 'hydra' => false, ]; #[\PHPUnit\Framework\Attributes\DataProvider('getExtractors')] diff --git a/src/State/ApiResource/Error.php b/src/State/ApiResource/Error.php index ec7e816824f..a69c7e6f97a 100644 --- a/src/State/ApiResource/Error.php +++ b/src/State/ApiResource/Error.php @@ -33,6 +33,7 @@ name: '_api_errors_problem', routeName: 'api_errors', outputFormats: ['json' => ['application/problem+json']], + hideHydraOperation: true, normalizationContext: [ 'groups' => ['jsonproblem'], 'skip_null_values' => true, @@ -51,6 +52,7 @@ new Operation( name: '_api_errors_jsonapi', routeName: 'api_errors', + hideHydraOperation: true, outputFormats: ['jsonapi' => ['application/vnd.api+json']], normalizationContext: [ 'groups' => ['jsonapi'], @@ -59,18 +61,21 @@ ), new Operation( name: '_api_errors', - routeName: 'api_errors' + routeName: 'api_errors', + hideHydraOperation: true, ), ], provider: 'api_platform.state.error_provider', graphQlOperations: [] )] +#[ApiProperty(property: 'traceAsString', hydra: false)] +#[ApiProperty(property: 'string', hydra: false)] class Error extends \Exception implements ProblemExceptionInterface, HttpExceptionInterface { public function __construct( private string $title, private string $detail, - #[ApiProperty(identifier: true)] private int $status, + #[ApiProperty(identifier: true, writable: false, initializable: false)] private int $status, ?array $originalTrace = null, private ?string $instance = null, private string $type = 'about:blank', @@ -98,9 +103,11 @@ public function getId(): string #[SerializedName('trace')] #[Groups(['trace'])] + #[ApiProperty(writable: false, initializable: false)] public ?array $originalTrace = null; #[Groups(['jsonld'])] + #[ApiProperty(writable: false, initializable: false)] public function getDescription(): ?string { return $this->detail; @@ -121,7 +128,6 @@ public function getHeaders(): array } #[Ignore] - #[ApiProperty(readable: false)] public function getStatusCode(): int { return $this->status; @@ -136,6 +142,7 @@ public function setHeaders(array $headers): void } #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] public function getType(): string { return $this->type; @@ -147,6 +154,7 @@ public function setType(string $type): void } #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] public function getTitle(): ?string { return $this->title; @@ -169,6 +177,7 @@ public function setStatus(int $status): void } #[Groups(['jsonld', 'jsonproblem', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] public function getDetail(): ?string { return $this->detail; @@ -180,6 +189,7 @@ public function setDetail(?string $detail = null): void } #[Groups(['jsonld', 'jsonproblem'])] + #[ApiProperty(writable: false, initializable: false)] public function getInstance(): ?string { return $this->instance; diff --git a/src/Validator/Exception/ValidationException.php b/src/Validator/Exception/ValidationException.php index ef38c3d7ec8..82631b92b6d 100644 --- a/src/Validator/Exception/ValidationException.php +++ b/src/Validator/Exception/ValidationException.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Validator\Exception; +use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\Error as ErrorOperation; use ApiPlatform\Metadata\ErrorResource; use ApiPlatform\Metadata\Exception\HttpExceptionInterface; @@ -36,14 +37,16 @@ openapi: false, uriVariables: ['id'], provider: 'api_platform.validator.state.error_provider', - shortName: 'ConstraintViolationList', + shortName: 'ConstraintViolation', operations: [ new ErrorOperation( name: '_api_validation_errors_problem', outputFormats: ['json' => ['application/problem+json']], - normalizationContext: ['groups' => ['json'], + normalizationContext: [ + 'groups' => ['json'], 'skip_null_values' => true, - ]), + ] + ), new ErrorOperation( name: '_api_validation_errors_hydra', outputFormats: ['jsonld' => ['application/problem+json']], @@ -61,6 +64,8 @@ ], graphQlOperations: [] )] +#[ApiProperty(property: 'traceAsString', hydra: false)] +#[ApiProperty(property: 'string', hydra: false)] class ValidationException extends RuntimeException implements ConstraintViolationListAwareExceptionInterface, \Stringable, ProblemExceptionInterface, HttpExceptionInterface, SymfonyHttpExceptionInterface { private int $status = 422; @@ -107,24 +112,28 @@ public function getId(): string } #[Groups(['jsonld'])] + #[ApiProperty(writable: false, initializable: false)] public function getDescription(): string { return $this->detail; } #[Groups(['jsonld', 'json', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] public function getType(): string { return '/validation_errors/'.$this->getId(); } #[Groups(['jsonld', 'json', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] public function getTitle(): ?string { return $this->errorTitle ?? 'An error occurred'; } #[Groups(['jsonld', 'json', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] private string $detail; public function getDetail(): ?string @@ -149,6 +158,7 @@ public function setStatus(int $status): void } #[Groups(['jsonld', 'json', 'jsonapi'])] + #[ApiProperty(writable: false, initializable: false)] public function getInstance(): ?string { return null; @@ -156,6 +166,7 @@ public function getInstance(): ?string #[SerializedName('violations')] #[Groups(['json', 'jsonld'])] + #[ApiProperty(jsonldContext: ['@type' => 'ConstraintViolationList'])] public function getConstraintViolationList(): ConstraintViolationListInterface { return $this->constraintViolationList;