Skip to content

Commit 217803d

Browse files
authored
refactor(jsonld): simplify @context building (#7952)
1 parent 7d6b830 commit 217803d

7 files changed

Lines changed: 45 additions & 149 deletions

File tree

phpstan.neon.dist

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ parameters:
5959
- tests/Fixtures/TestBundle/Document/
6060
- tests/Fixtures/TestBundle/Entity/
6161
- src/OpenApi/Factory/OpenApiFactory.php
62+
- src/JsonLd/Serializer/ObjectNormalizer.php
6263
-
6364
message: '#is never assigned .* so it can be removed from the property type.#'
6465
paths:

src/JsonLd/ContextBuilder.php

Lines changed: 8 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -82,19 +82,8 @@ public function getResourceContext(string $resourceClass, int $referenceType = U
8282
{
8383
/** @var HttpOperation $operation */
8484
$operation = $this->resourceMetadataFactory->create($resourceClass)->getOperation(null, false, true);
85-
if (null === $shortName = $operation->getShortName()) {
86-
return [];
87-
}
88-
89-
$context = $operation->getNormalizationContext();
90-
if ($context['iri_only'] ?? false) {
91-
$context = $this->getBaseContext($referenceType);
92-
$context[$this->getHydraPrefix($context).'member']['@type'] = '@id';
93-
94-
return $context;
95-
}
9685

97-
return $this->getResourceContextWithShortname($resourceClass, $referenceType, $shortName, $operation);
86+
return $this->getResourceContextFromOperation($operation, $resourceClass, $referenceType);
9887
}
9988

10089
/**
@@ -103,11 +92,8 @@ public function getResourceContext(string $resourceClass, int $referenceType = U
10392
public function getResourceContextUri(string $resourceClass, ?int $referenceType = null): string
10493
{
10594
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass)[0];
106-
if (null === $referenceType) {
107-
$referenceType = $resourceMetadata->getUrlGenerationStrategy();
108-
}
10995

110-
return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $resourceMetadata->getShortName()], $referenceType ?? UrlGeneratorInterface::ABS_PATH);
96+
return $this->generateContextUri($resourceMetadata->getShortName(), $referenceType ?? $resourceMetadata->getUrlGenerationStrategy());
11197
}
11298

11399
/**
@@ -155,12 +141,6 @@ public function getAnonymousResourceContext(object $object, array $context = [],
155141
unset($jsonLdContext['@context']);
156142
}
157143

158-
// here the object can be different from the resource given by the $context['api_resource'] value
159-
// TODO: this is probably not used anymore and is slow we get that @type way earlier, remove this
160-
if (isset($context['api_resource'])) {
161-
$jsonLdContext['@type'] = $this->resourceMetadataFactory->create($this->getObjectClass($context['api_resource']))[0]->getShortName();
162-
}
163-
164144
return $jsonLdContext;
165145
}
166146

@@ -169,11 +149,7 @@ public function getAnonymousResourceContext(object $object, array $context = [],
169149
*/
170150
public function getResourceContextUriFromOperation(HttpOperation $operation, ?int $referenceType = null): string
171151
{
172-
if (null === $referenceType) {
173-
$referenceType = $operation->getUrlGenerationStrategy();
174-
}
175-
176-
return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $operation->getShortName()], $referenceType ?? UrlGeneratorInterface::ABS_PATH);
152+
return $this->generateContextUri($operation->getShortName(), $referenceType ?? $operation->getUrlGenerationStrategy());
177153
}
178154

179155
/**
@@ -196,6 +172,11 @@ public function getResourceContextFromOperation(HttpOperation $operation, string
196172
return $this->getResourceContextWithShortname($resourceClass, $referenceType, $shortName, $operation);
197173
}
198174

175+
private function generateContextUri(?string $shortName, ?int $referenceType): string
176+
{
177+
return $this->urlGenerator->generate('api_jsonld_context', ['shortName' => $shortName], $referenceType ?? UrlGeneratorInterface::ABS_PATH);
178+
}
179+
199180
private function getResourceContextWithShortname(string $resourceClass, int $referenceType, string $shortName, ?HttpOperation $operation = null): array
200181
{
201182
$context = $this->getBaseContext($referenceType);

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,18 @@ public function normalize(mixed $data, ?string $format = null, array $context =
152152
return $normalizedData;
153153
}
154154

155+
if (!isset($metadata['@type']) && null !== ($type = $this->resolveType($resourceClass, $isResourceClass, $context))) {
156+
$metadata['@type'] = $type;
157+
}
158+
159+
return $metadata + $normalizedData;
160+
}
161+
162+
/**
163+
* @return string|array<int, string>|null
164+
*/
165+
private function resolveType(string $resourceClass, bool $isResourceClass, array $context): string|array|null
166+
{
155167
$operation = $context['operation'] ?? null;
156168

157169
if ($this->operationMetadataFactory && isset($context['item_uri_template']) && !$operation) {
@@ -162,30 +174,31 @@ public function normalize(mixed $data, ?string $format = null, array $context =
162174
$operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation();
163175
}
164176

165-
if (!isset($metadata['@type']) && $operation) {
166-
$types = $operation instanceof HttpOperation ? $operation->getTypes() : null;
167-
if (null === $types) {
168-
// TODO: 5.x break on this as this looks wrong, CollectionReferencingItem returns an IRI that point through
169-
// ItemReferencedInCollection but it returns a CollectionReferencingItem therefore we should use the current
170-
// object's class Type and not rely on operation ?
171-
if (isset($context['item_uri_template'])) {
172-
// When the operation comes from item_uri_template, use its shortName directly
173-
// as $resourceClass refers to the collection resource, not the item resource
177+
if (!$operation) {
178+
return null;
179+
}
180+
181+
$types = $operation instanceof HttpOperation ? $operation->getTypes() : null;
182+
if (null === $types) {
183+
// TODO: 5.x break on this as this looks wrong, CollectionReferencingItem returns an IRI that point through
184+
// ItemReferencedInCollection but it returns a CollectionReferencingItem therefore we should use the current
185+
// object's class Type and not rely on operation ?
186+
if (isset($context['item_uri_template'])) {
187+
// When the operation comes from item_uri_template, use its shortName directly
188+
// as $resourceClass refers to the collection resource, not the item resource
189+
$types = [$operation->getShortName()];
190+
} else {
191+
// Use resource-level shortName to avoid operation-specific overrides
192+
$typeClass = $isResourceClass ? $resourceClass : ($operation->getClass() ?? $resourceClass);
193+
try {
194+
$types = [$this->resourceMetadataCollectionFactory->create($typeClass)[0]->getShortName()];
195+
} catch (\Exception) {
174196
$types = [$operation->getShortName()];
175-
} else {
176-
// Use resource-level shortName to avoid operation-specific overrides
177-
$typeClass = $isResourceClass ? $resourceClass : ($operation->getClass() ?? $resourceClass);
178-
try {
179-
$types = [$this->resourceMetadataCollectionFactory->create($typeClass)[0]->getShortName()];
180-
} catch (\Exception) {
181-
$types = [$operation->getShortName()];
182-
}
183197
}
184198
}
185-
$metadata['@type'] = 1 === \count($types) ? $types[0] : $types;
186199
}
187200

188-
return $metadata + $normalizedData;
201+
return 1 === \count($types) ? $types[0] : $types;
189202
}
190203

191204
/**

src/JsonLd/Serializer/JsonLdContextTrait.php

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,7 @@ private function addJsonLdContext(ContextBuilderInterface $contextBuilder, strin
5959

6060
private function createJsonLdContext(AnonymousContextBuilderInterface $contextBuilder, object $object, array &$context): array
6161
{
62-
$anonymousContext = ($context['output'] ?? []) + [
63-
'api_resource' => $context['api_resource'] ?? null,
64-
];
62+
$anonymousContext = $context['output'] ?? [];
6563

6664
if (isset($context['item_uri_template'])) {
6765
$anonymousContext['item_uri_template'] = $context['item_uri_template'];

src/JsonLd/Serializer/ObjectNormalizer.php

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
namespace ApiPlatform\JsonLd\Serializer;
1515

1616
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
17-
use ApiPlatform\Metadata\Exception\InvalidArgumentException;
1817
use ApiPlatform\Metadata\IriConverterInterface;
1918
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
2019

@@ -53,11 +52,6 @@ public function getSupportedTypes(?string $format): array
5352
*/
5453
public function normalize(mixed $data, ?string $format = null, array $context = []): array|string|int|float|bool|\ArrayObject|null
5554
{
56-
if (isset($context['api_resource'])) {
57-
$originalResource = $context['api_resource'];
58-
unset($context['api_resource']);
59-
}
60-
6155
/*
6256
* Converts the normalized data array of a resource into an IRI, if the
6357
* normalized data array is empty.
@@ -75,15 +69,6 @@ public function normalize(mixed $data, ?string $format = null, array $context =
7569
return $normalizedData;
7670
}
7771

78-
if (isset($originalResource)) {
79-
try {
80-
$context['output']['iri'] = $this->iriConverter->getIriFromResource($originalResource);
81-
} catch (InvalidArgumentException) {
82-
// The original resource has no identifiers
83-
}
84-
$context['api_resource'] = $originalResource;
85-
}
86-
8772
$metadata = $this->createJsonLdContext($this->anonymousContextBuilder, $data, $context);
8873

8974
return $metadata + $normalizedData;

tests/JsonLd/ContextBuilderTest.php

Lines changed: 0 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -226,57 +226,6 @@ public function testAnonymousResourceContextWithIri(): void
226226
$this->assertEquals($expected, $contextBuilder->getAnonymousResourceContext($output, ['iri' => '/dummies', 'name' => 'Dummy']));
227227
}
228228

229-
public function testAnonymousResourceContextWithApiResource(): void
230-
{
231-
$output = new OutputDto();
232-
$this->propertyNameCollectionFactoryProphecy->create(OutputDto::class)->willReturn(new PropertyNameCollection(['dummyPropertyA']));
233-
$this->propertyMetadataFactoryProphecy->create(OutputDto::class, 'dummyPropertyA', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('Dummy property A')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
234-
$this->urlGeneratorProphecy->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL)->willReturn('');
235-
236-
$this->resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [
237-
(new ApiResource())
238-
->withShortName('Dummy')
239-
->withOperations(new Operations(['get' => (new Get())->withShortName('Dummy')])),
240-
]));
241-
242-
$contextBuilder = new ContextBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->urlGeneratorProphecy->reveal());
243-
244-
$expected = [
245-
'@context' => [
246-
'@vocab' => '#',
247-
'hydra' => 'http://www.w3.org/ns/hydra/core#',
248-
'dummyPropertyA' => 'OutputDto/dummyPropertyA',
249-
],
250-
'@id' => '/dummies',
251-
'@type' => 'Dummy',
252-
];
253-
254-
$this->assertEquals($expected, $contextBuilder->getAnonymousResourceContext($output, ['iri' => '/dummies', 'name' => 'Dummy', 'api_resource' => new Dummy()]));
255-
}
256-
257-
public function testAnonymousResourceContextWithApiResourceHavingContext(): void
258-
{
259-
$output = new OutputDto();
260-
$this->propertyNameCollectionFactoryProphecy->create(OutputDto::class)->willReturn(new PropertyNameCollection(['dummyPropertyA']));
261-
$this->propertyMetadataFactoryProphecy->create(OutputDto::class, 'dummyPropertyA', Argument::type('array'))->willReturn((new ApiProperty())->withNativeType(Type::string())->withDescription('Dummy property A')->withReadable(true)->withWritable(true)->withReadableLink(true)->withWritableLink(true));
262-
$this->urlGeneratorProphecy->generate('api_doc', ['_format' => 'jsonld'], UrlGeneratorInterface::ABS_URL)->willReturn('');
263-
264-
$this->resourceMetadataCollectionFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadataCollection('Dummy', [
265-
(new ApiResource())
266-
->withShortName('Dummy')
267-
->withOperations(new Operations(['get' => (new Get())->withShortName('Dummy')])),
268-
]));
269-
270-
$contextBuilder = new ContextBuilder($this->resourceNameCollectionFactoryProphecy->reveal(), $this->resourceMetadataCollectionFactoryProphecy->reveal(), $this->propertyNameCollectionFactoryProphecy->reveal(), $this->propertyMetadataFactoryProphecy->reveal(), $this->urlGeneratorProphecy->reveal());
271-
272-
$expected = [
273-
'@id' => '/dummies',
274-
'@type' => 'Dummy',
275-
];
276-
277-
$this->assertEquals($expected, $contextBuilder->getAnonymousResourceContext($output, ['iri' => '/dummies', 'name' => 'Dummy', 'api_resource' => new Dummy(), 'has_context' => true]));
278-
}
279-
280229
public function testResourceContextWithoutHydraPrefix(): void
281230
{
282231
$this->resourceMetadataCollectionFactoryProphecy->create($this->entityClass)->willReturn(new ResourceMetadataCollection('DummyEntity', [

tests/JsonLd/Serializer/ObjectNormalizerTest.php

Lines changed: 3 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -86,50 +86,19 @@ public function testNormalizeEmptyArray(): void
8686
$this->assertEquals([], $normalizer->normalize($dummy));
8787
}
8888

89-
public function testNormalizeWithOutput(): void
89+
public function testNormalizeWithJsonLdContextSet(): void
9090
{
9191
$dummy = new Dummy();
9292
$dummy->setName('hello');
9393

9494
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
95-
$iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummy/1234');
9695

9796
$serializerProphecy = $this->prophesize(SerializerInterface::class);
9897
$serializerProphecy->willImplement(NormalizerInterface::class);
9998
$serializerProphecy->normalize($dummy, null, Argument::type('array'))->willReturn(['name' => 'hello']);
10099

101100
$contextBuilderProphecy = $this->prophesize(AnonymousContextBuilderInterface::class);
102-
$contextBuilderProphecy->getAnonymousResourceContext($dummy, ['api_resource' => $dummy, 'iri' => '/dummy/1234'])->shouldBeCalled()->willReturn(['@id' => '/dummy/1234', '@type' => 'Dummy', '@context' => []]);
103-
104-
$normalizer = new ObjectNormalizer(
105-
$serializerProphecy->reveal(),
106-
$iriConverterProphecy->reveal(),
107-
$contextBuilderProphecy->reveal()
108-
);
109-
110-
$expected = [
111-
'@context' => [],
112-
'@id' => '/dummy/1234',
113-
'@type' => 'Dummy',
114-
'name' => 'hello',
115-
];
116-
$this->assertEquals($expected, $normalizer->normalize($dummy, null, ['api_resource' => $dummy]));
117-
}
118-
119-
public function testNormalizeWithContext(): void
120-
{
121-
$dummy = new Dummy();
122-
$dummy->setName('hello');
123-
124-
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
125-
$iriConverterProphecy->getIriFromResource($dummy)->willReturn('/dummy/1234');
126-
127-
$serializerProphecy = $this->prophesize(SerializerInterface::class);
128-
$serializerProphecy->willImplement(NormalizerInterface::class);
129-
$serializerProphecy->normalize($dummy, null, Argument::type('array'))->willReturn(['name' => 'hello']);
130-
131-
$contextBuilderProphecy = $this->prophesize(AnonymousContextBuilderInterface::class);
132-
$contextBuilderProphecy->getAnonymousResourceContext($dummy, ['api_resource' => $dummy, 'has_context' => true, 'iri' => '/dummy/1234'])->shouldBeCalled()->willReturn(['@id' => '/dummy/1234', '@type' => 'Dummy']);
101+
$contextBuilderProphecy->getAnonymousResourceContext($dummy, ['has_context' => true])->shouldBeCalled()->willReturn(['@id' => '/dummy/1234', '@type' => 'Dummy']);
133102

134103
$normalizer = new ObjectNormalizer(
135104
$serializerProphecy->reveal(),
@@ -142,6 +111,6 @@ public function testNormalizeWithContext(): void
142111
'@type' => 'Dummy',
143112
'name' => 'hello',
144113
];
145-
$this->assertEquals($expected, $normalizer->normalize($dummy, null, ['api_resource' => $dummy, 'jsonld_has_context' => true]));
114+
$this->assertEquals($expected, $normalizer->normalize($dummy, null, ['jsonld_has_context' => true]));
146115
}
147116
}

0 commit comments

Comments
 (0)