From 86ee2d082792d95495423d6df989770a4a666b14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20H=C3=A9bert?= Date: Fri, 18 Aug 2023 17:08:14 +0200 Subject: [PATCH] feat: handle uriTemplate on property for HAL format --- features/hal/collection_uri_template.feature | 52 +++++++++++++++++++ src/Hal/Serializer/ItemNormalizer.php | 30 ++++++++++- src/Serializer/AbstractItemNormalizer.php | 3 +- .../Entity/PropertyCollectionIriOnly.php | 2 +- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 features/hal/collection_uri_template.feature diff --git a/features/hal/collection_uri_template.feature b/features/hal/collection_uri_template.feature new file mode 100644 index 00000000000..f510739fcba --- /dev/null +++ b/features/hal/collection_uri_template.feature @@ -0,0 +1,52 @@ +@php8 +@v3 +Feature: Exposing a property being a collection of resources + can return an IRI instead of an array + when the uriTemplate is set on the ApiProperty attribute + + Scenario: Retrieve Resource with uriTemplate collection Property + Given there are propertyCollectionIriOnly with relations + When I add "Accept" header equal to "application/hal+json" + And I send a "GET" request to "/property_collection_iri_onlies/1" + Then the response status code should be 200 + And the response should be in JSON + And the JSON should be valid according to the JSON HAL schema + And the header "Content-Type" should be equal to "application/hal+json; charset=utf-8" + And the JSON should be equal to: + """ + { + "_links": { + "self": { + "href": "/property_collection_iri_onlies/1" + }, + "propertyCollectionIriOnlyRelation": { + "href": "/property-collection-relations" + }, + "iterableIri": { + "href": "/parent/1/another-collection-operations" + } + }, + "_embedded": { + "propertyCollectionIriOnlyRelation": [ + { + "_links": { + "self": { + "href": "/property_collection_iri_only_relations/1" + } + }, + "name": "relation" + } + ], + "iterableIri": [ + { + "_links": { + "self": { + "href": "/property_collection_iri_only_relations/9999" + } + }, + "name": "Michel" + } + ] + } + } + """ diff --git a/src/Hal/Serializer/ItemNormalizer.php b/src/Hal/Serializer/ItemNormalizer.php index d54d601e91a..a1d8aa6e751 100644 --- a/src/Hal/Serializer/ItemNormalizer.php +++ b/src/Hal/Serializer/ItemNormalizer.php @@ -14,6 +14,7 @@ namespace ApiPlatform\Hal\Serializer; use ApiPlatform\Api\UrlGeneratorInterface; +use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Util\ClassInfoTrait; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Serializer\CacheKeyTrait; @@ -166,7 +167,28 @@ private function getComponents(object $object, ?string $format, array $context): continue; } - $relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many']; + $relation = ['name' => $attribute, 'cardinality' => $isOne ? 'one' : 'many', 'uriTemplate' => null]; + + // if we specify the uriTemplate, generates its value for link definition + // @see ApiPlatform\Serializer\AbstractItemNormalizer:getAttributeValue logic for intentional duplicate content + if ($isMany && $itemUriTemplate = $propertyMetadata->getUriTemplate()) { + $attributeValue = $this->propertyAccessor->getValue($object, $attribute); + $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); + $childContext = $this->createChildContext($context, $attribute, $format); + unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); + + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation( + operationName: $itemUriTemplate, + forceCollection: true, + httpOperation: true + ); + + if ($operation instanceof GetCollection) { + $relation['uriTemplate'] = $this->iriConverter->getIriFromResource($object, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + } + } + + if ($propertyMetadata->isReadableLink()) { $components['embedded'][] = $relation; } @@ -215,6 +237,12 @@ private function populateRelation(array $data, object $object, ?string $format, $relationName = $this->nameConverter->normalize($relationName, $class, $format, $context); } + // if we specify the uriTemplate, then the link takes the uriTemplate defined. + if ('links' === $type && $itemUriTemplate = $relation['uriTemplate']) { + $data[$key][$relationName]['href'] = $itemUriTemplate; + continue; + } + if ('one' === $relation['cardinality']) { if ('links' === $type) { $data[$key][$relationName]['href'] = $this->getRelationIri($attributeValue); diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 60e365a5af5..55209343ef6 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -635,7 +635,8 @@ protected function getAttributeValue(object $object, string $attribute, string $ $resourceClass = $this->resourceClassResolver->getResourceClass($attributeValue, $className); $childContext = $this->createChildContext($this->createOperationContext($context, $resourceClass), $attribute, $format); - if ($itemUriTemplate = $propertyMetadata->getUriTemplate()) { + // @see ApiPlatform\Hal\Serializer\ItemNormalizer:getComponents logic for intentional duplicate content + if ($format === 'jsonld' && $itemUriTemplate = $propertyMetadata->getUriTemplate()) { $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation( operationName: $itemUriTemplate, forceCollection: true, diff --git a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php index a763912296d..65c6424feb7 100644 --- a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php +++ b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php @@ -98,7 +98,7 @@ public function getIterableIri(): array $propertyCollectionIriOnlyRelation = new PropertyCollectionIriOnlyRelation(); $propertyCollectionIriOnlyRelation->name = 'Michel'; - $this->iterableIri[] = $propertyCollectionIriOnlyRelation; + $this->iterableIri = [$propertyCollectionIriOnlyRelation]; return $this->iterableIri; }