diff --git a/features/jsonld/non_resource.feature b/features/jsonld/non_resource.feature index 654d90d35bf..fefa71f8609 100644 --- a/features/jsonld/non_resource.feature +++ b/features/jsonld/non_resource.feature @@ -38,6 +38,7 @@ Feature: JSON-LD non-resource handling } } """ + And the JSON node "notAResource.@id" should not exist Scenario: Get a resource containing a raw object with selected properties Given there are 1 dummy objects with relatedDummy and its thirdLevel @@ -123,3 +124,11 @@ Feature: JSON-LD non-resource handling "id": 1 } """ + + @php8 + Scenario: Get a generated id + When I send a "GET" request to "/genids/1" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON node "totalPrice.@id" should exist diff --git a/src/Hydra/Serializer/CollectionNormalizer.php b/src/Hydra/Serializer/CollectionNormalizer.php index 91a2903edca..af5791decb6 100644 --- a/src/Hydra/Serializer/CollectionNormalizer.php +++ b/src/Hydra/Serializer/CollectionNormalizer.php @@ -87,6 +87,7 @@ public function normalize($object, $format = null, array $context = []): array $resourceClass = $this->resourceClassResolver->getResourceClass($object, $context['resource_class']); $context = $this->initContext($resourceClass, $context); + $context['api_collection_sub_level'] = true; $data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context); if ($this->iriConverter instanceof LegacyIriConverterInterface) { diff --git a/src/JsonLd/ContextBuilder.php b/src/JsonLd/ContextBuilder.php index ea04f293d3f..c7e2e3c3608 100644 --- a/src/JsonLd/ContextBuilder.php +++ b/src/JsonLd/ContextBuilder.php @@ -200,6 +200,14 @@ public function getAnonymousResourceContext($object, array $context = [], int $r } } + if ($this->iriConverter && isset($context['gen_id']) && true === $context['gen_id']) { + $jsonLdContext['@id'] = $this->iriConverter->getIriFromResource($object); + } + + if (false === ($context['iri'] ?? null)) { + trigger_deprecation('api-platform/core', '2.7', 'An anonymous resource will use a Skolem IRI in API Platform 3.0. Use #[ApiProperty(genId: false)] to keep this behavior in 3.0.'); + } + if ($context['has_context'] ?? false) { unset($jsonLdContext['@context']); } diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index 8482f236a5b..1102f7b48ba 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -89,6 +89,12 @@ public function normalize($object, $format = null, array $context = []) $context = $this->initContext($resourceClass, $context); $metadata = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context); } elseif ($this->contextBuilder instanceof AnonymousContextBuilderInterface) { + if ($context['api_collection_sub_level'] ?? false) { + unset($context['api_collection_sub_level']); + $context['output']['genid'] = true; + $context['output']['iri'] = null; + } + // We should improve what's behind the context creation, its probably more complicated then it should $metadata = $this->createJsonLdContext($this->contextBuilder, $object, $context); } diff --git a/src/JsonLd/Serializer/JsonLdContextTrait.php b/src/JsonLd/Serializer/JsonLdContextTrait.php index 296c6476143..43612ccc785 100644 --- a/src/JsonLd/Serializer/JsonLdContextTrait.php +++ b/src/JsonLd/Serializer/JsonLdContextTrait.php @@ -51,7 +51,7 @@ private function createJsonLdContext(AnonymousContextBuilderInterface $contextBu { // We're in a collection, don't add the @context part if (isset($context['jsonld_has_context'])) { - return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null, 'has_context' => true]); + return $contextBuilder->getAnonymousResourceContext($object, ($context['output'] ?? []) + ['api_resource' => $context['api_resource'] ?? null, 'has_context' => true, 'iri' => false]); } $context['jsonld_has_context'] = true; diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index a9641a931c1..cd62a5656ca 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -92,6 +92,7 @@ final class ApiProperty private $schema; private $initializable; + private $genId; /** * @var string[] @@ -146,6 +147,7 @@ public function __construct( ?array $builtinTypes = null, ?array $schema = null, ?bool $initializable = null, + ?bool $genId = null, $iris = null, @@ -175,6 +177,7 @@ public function __construct( $this->builtinTypes = $builtinTypes; $this->schema = $schema; $this->initializable = $initializable; + $this->genId = $genId; $this->iris = $iris; $this->extraProperties = $extraProperties; } @@ -507,4 +510,20 @@ public function withIris($iris): self return $metadata; } + + /** + * Whether to generate a skolem iri on anonymous resources. + */ + public function getGenId() + { + return $this->genId; + } + + public function withGenId(bool $genId): self + { + $metadata = clone $this; + $metadata->genId = $genId; + + return $metadata; + } } diff --git a/src/Metadata/Extractor/XmlPropertyExtractor.php b/src/Metadata/Extractor/XmlPropertyExtractor.php index f5fcabf023c..2da5d1dd5dc 100644 --- a/src/Metadata/Extractor/XmlPropertyExtractor.php +++ b/src/Metadata/Extractor/XmlPropertyExtractor.php @@ -71,6 +71,7 @@ protected function extractPath(string $path) 'initializable' => $this->phpize($property, 'initializable', 'bool'), 'extraProperties' => $this->buildExtraProperties($property, 'extraProperties'), 'iris' => $this->buildArrayValue($property, 'iri'), + 'genId' => $this->phpize($property, 'genId', 'bool'), ]; } } diff --git a/src/Metadata/Extractor/YamlPropertyExtractor.php b/src/Metadata/Extractor/YamlPropertyExtractor.php index ca14895a0e6..dd1231ca2aa 100644 --- a/src/Metadata/Extractor/YamlPropertyExtractor.php +++ b/src/Metadata/Extractor/YamlPropertyExtractor.php @@ -92,6 +92,7 @@ private function buildProperties(array $resourcesYaml): void 'example' => $propertyValues['example'] ?? null, 'builtinTypes' => $this->buildAttribute($propertyValues, 'builtinTypes'), 'schema' => $this->buildAttribute($propertyValues, 'schema'), + 'genId' => $this->phpize($propertyValues, 'genId', 'bool'), ]; } } diff --git a/src/Metadata/Extractor/schema/properties.xsd b/src/Metadata/Extractor/schema/properties.xsd index 86422068b7f..f25266eba22 100644 --- a/src/Metadata/Extractor/schema/properties.xsd +++ b/src/Metadata/Extractor/schema/properties.xsd @@ -43,6 +43,7 @@ + diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 279edf8fd3a..3fc9b19d7c0 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -831,11 +831,9 @@ protected function getAttributeValue($object, $attribute, $format = null, array unset($childContext['iri'], $childContext['uri_variables']); if ($propertyMetadata instanceof PropertyMetadata) { - $childContext['output']['iri'] = $propertyMetadata->getIri(); + $childContext['output']['iri'] = $propertyMetadata->getIri() ?? false; } else { - if (null !== ($propertyIris = $propertyMetadata->getIris())) { - $childContext['output']['iri'] = 1 === \count($propertyIris) ? $propertyIris[0] : $propertyIris; - } + $childContext['output']['gen_id'] = $propertyMetadata->getGenId() ?? false; } return $this->serializer->normalize($attributeValue, $format, $childContext); diff --git a/tests/Fixtures/TestBundle/Model/GenId.php b/tests/Fixtures/TestBundle/Model/GenId.php new file mode 100644 index 00000000000..58f40916dfd --- /dev/null +++ b/tests/Fixtures/TestBundle/Model/GenId.php @@ -0,0 +1,35 @@ + + * + * 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\Model; + +use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\Get; +use ApiPlatform\Metadata\Operation; + +#[Get('/genids/{id}', provider: [GenId::class, 'getData'])] +class GenId +{ + #[ApiProperty(genId: true)] + public MonetaryAmount $totalPrice; + + public function __construct(public int $id) + { + $this->totalPrice = new MonetaryAmount(1000.01); + } + + public static function getData(Operation $operation, array $uriVariables = [], array $context = []): self + { + return new self($uriVariables['id']); + } +} diff --git a/tests/Fixtures/TestBundle/Model/MonetaryAmount.php b/tests/Fixtures/TestBundle/Model/MonetaryAmount.php new file mode 100644 index 00000000000..52adef125f1 --- /dev/null +++ b/tests/Fixtures/TestBundle/Model/MonetaryAmount.php @@ -0,0 +1,21 @@ + + * + * 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\Model; + +class MonetaryAmount +{ + public function __construct(public float $value = 0.0, public string $currency = 'EUR', public float $minValue = 0.0) + { + } +} diff --git a/tests/Hydra/Serializer/CollectionNormalizerTest.php b/tests/Hydra/Serializer/CollectionNormalizerTest.php index b394f295ec8..428fd791ba7 100644 --- a/tests/Hydra/Serializer/CollectionNormalizerTest.php +++ b/tests/Hydra/Serializer/CollectionNormalizerTest.php @@ -327,6 +327,7 @@ private function normalizePaginator($partial = false) 'jsonld_has_context' => true, 'api_sub_level' => true, 'resource_class' => 'Foo', + 'api_collection_sub_level' => true, ])->willReturn(['name' => 'Kévin', 'friend' => 'Smail']); $normalizer = new CollectionNormalizer($contextBuilder->reveal(), $resourceClassResolverProphecy->reveal(), $iriConvert->reveal()); diff --git a/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php b/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php index 52248aed1ad..9aee0b5896f 100644 --- a/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php +++ b/tests/Metadata/Extractor/Adapter/XmlPropertyAdapter.php @@ -42,6 +42,7 @@ final class XmlPropertyAdapter implements PropertyAdapterInterface 'securityPostDenormalize', 'initializable', 'iris', + 'genId', ]; /** diff --git a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php index 84677a9912d..e310c802468 100644 --- a/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php +++ b/tests/Metadata/Extractor/PropertyMetadataCompatibilityTest.php @@ -75,6 +75,7 @@ final class PropertyMetadataCompatibilityTest extends TestCase 'custom_property' => 'Lorem ipsum dolor sit amet', ], 'iris' => ['https://schema.org/totalPrice'], + 'genId' => true, ]; /**