Skip to content

Commit 73027e0

Browse files
committed
fix(jsonld): various json streamer fixes
1 parent 6db55be commit 73027e0

File tree

6 files changed

+172
-12
lines changed

6 files changed

+172
-12
lines changed

src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,55 @@
1515

1616
use ApiPlatform\Hydra\Collection;
1717
use ApiPlatform\Metadata\Exception\RuntimeException;
18+
use ApiPlatform\Metadata\HttpOperation;
19+
use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
20+
use ApiPlatform\Metadata\ResourceClassResolverInterface;
1821
use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
1922
use Symfony\Component\TypeInfo\Type;
2023

2124
final class TypeValueTransformer implements ValueTransformerInterface
2225
{
26+
public function __construct(
27+
private readonly ResourceClassResolverInterface $resourceClassResolver,
28+
private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
29+
) {
30+
}
31+
2332
public function transform(mixed $value, array $options = []): mixed
2433
{
2534
if ($options['_current_object'] instanceof Collection) {
2635
return 'Collection';
2736
}
2837

29-
if (!isset($options['operation'])) {
30-
throw new RuntimeException('Operation is not defined');
38+
$dataClass = \is_object($options['data']) ? $options['data']::class : null;
39+
if (($currentClass = $options['_current_object']::class) === $dataClass) {
40+
if (!isset($options['operation'])) {
41+
throw new RuntimeException('Operation is not defined');
42+
}
43+
44+
return $this->getOperationType($options['operation']);
45+
}
46+
47+
if (!$this->resourceClassResolver->isResourceClass($currentClass)) {
48+
return null;
3149
}
3250

33-
return $options['operation']->getShortName();
51+
$op = $this->resourceMetadataCollectionFactory->create($currentClass)->getOperation(httpOperation: true);
52+
53+
return $this->getOperationType($op);
3454
}
3555

3656
public static function getStreamValueType(): Type
3757
{
3858
return Type::string();
3959
}
60+
61+
private function getOperationType(HttpOperation $operation): array|string
62+
{
63+
if (($t = $operation->getTypes()) && 1 === \count($t)) {
64+
return $operation->getTypes()[0];
65+
}
66+
67+
return $t ?: $operation->getShortname();
68+
}
4069
}

src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515

1616
use ApiPlatform\Hydra\Collection;
1717
use ApiPlatform\Hydra\IriTemplate;
18+
use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
19+
use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
1820
use ApiPlatform\Metadata\ResourceClassResolverInterface;
1921
use ApiPlatform\Metadata\Util\TypeHelper;
2022
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadata;
@@ -26,6 +28,8 @@ final class WritePropertyMetadataLoader implements PropertyMetadataLoaderInterfa
2628
public function __construct(
2729
private readonly PropertyMetadataLoaderInterface $loader,
2830
private readonly ResourceClassResolverInterface $resourceClassResolver,
31+
private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
32+
private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
2933
) {
3034
}
3135

@@ -47,23 +51,47 @@ public function load(string $className, array $options = [], array $context = []
4751
return $properties;
4852
}
4953

50-
$properties['@id'] = new PropertyMetadata(
51-
'id', // virtual property
52-
Type::mixed(), // virtual property
53-
['api_platform.jsonld.json_streamer.write.value_transformer.iri'],
54-
);
54+
$originalClassName = TypeHelper::getClassName($context['original_type']);
55+
$hasIri = true;
56+
$virtualProperty = 'id';
57+
58+
foreach ($this->propertyNameCollectionFactory->create($originalClassName) as $property) {
59+
$propertyMetadata = $this->propertyMetadataFactory->create($originalClassName, $property);
60+
if ($propertyMetadata->isIdentifier()) {
61+
$virtualProperty = $property;
62+
}
63+
64+
if ($className === $originalClassName) {
65+
continue;
66+
}
67+
68+
if ($propertyMetadata->getNativeType()->isIdentifiedBy($className)) {
69+
$hasIri = $propertyMetadata->getGenId();
70+
$virtualProperty = iterator_to_array($this->propertyNameCollectionFactory->create($className))[0];
71+
}
72+
}
73+
74+
if ($hasIri) {
75+
$properties['@id'] = new PropertyMetadata(
76+
$virtualProperty, // virtual property
77+
Type::mixed(), // virtual property
78+
['api_platform.jsonld.json_streamer.write.value_transformer.iri'],
79+
);
80+
}
5581

5682
$properties['@type'] = new PropertyMetadata(
57-
'id', // virtual property
83+
$virtualProperty, // virtual property
5884
Type::mixed(), // virtual property
5985
['api_platform.jsonld.json_streamer.write.value_transformer.type'],
6086
);
6187

62-
$originalClassName = TypeHelper::getClassName($context['original_type']);
88+
if ($className !== $originalClassName) {
89+
return $properties;
90+
}
6391

6492
if (Collection::class === $originalClassName || ($this->resourceClassResolver->isResourceClass($originalClassName) && !isset($context['generated_classes'][Collection::class]))) {
6593
$properties['@context'] = new PropertyMetadata(
66-
'id', // virual property
94+
$virtualProperty, // virual property
6795
Type::string(), // virtual property
6896
['api_platform.jsonld.json_streamer.write.value_transformer.context'],
6997
);

src/Symfony/Bundle/Resources/config/json_streamer/common.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
<service id="api_platform.jsonld.json_streamer.write.property_metadata_loader" class="ApiPlatform\JsonLd\JsonStreamer\WritePropertyMetadataLoader">
2020
<argument type="service" id="json_streamer.write.property_metadata_loader" />
2121
<argument type="service" id="api_platform.resource_class_resolver" />
22+
<argument type="service" id="api_platform.metadata.property.name_collection_factory" />
23+
<argument type="service" id="api_platform.metadata.property.metadata_factory" />
2224
</service>
2325

2426
<service id="api_platform.jsonld.json_streamer.write.value_transformer.iri" class="ApiPlatform\JsonLd\JsonStreamer\ValueTransformer\IriValueTransformer">
@@ -27,6 +29,8 @@
2729
</service>
2830

2931
<service id="api_platform.jsonld.json_streamer.write.value_transformer.type" class="ApiPlatform\JsonLd\JsonStreamer\ValueTransformer\TypeValueTransformer">
32+
<argument type="service" id="api_platform.resource_class_resolver" />
33+
<argument type="service" id="api_platform.metadata.resource.metadata_collection_factory" />
3034
<tag name="json_streamer.value_transformer"/>
3135
</service>
3236

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\ApiResource;
18+
19+
#[ApiResource(types: 'https://schema.org/AggregateRating', operations: [])]
20+
final class AggregateRating
21+
{
22+
public function __construct(
23+
#[ApiProperty(iris: ['https://schema.org/ratingValue'])]
24+
public float $ratingValue,
25+
#[ApiProperty(iris: ['https://schema.org/reviewCount'])]
26+
public int $reviewCount,
27+
) {
28+
}
29+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the API Platform project.
5+
*
6+
* (c) Kévin Dunglas <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace ApiPlatform\Tests\Fixtures\TestBundle\ApiResource;
15+
16+
use ApiPlatform\Metadata\ApiProperty;
17+
use ApiPlatform\Metadata\Get;
18+
19+
#[Get(
20+
types: ['https://schema.org/Product'],
21+
uriTemplate: '/json-stream-products/{code}',
22+
uriVariables: ['code'],
23+
provider: [self::class, 'provide'],
24+
jsonStream: true
25+
)]
26+
class Product
27+
{
28+
#[ApiProperty(identifier: true)]
29+
public string $code;
30+
31+
#[ApiProperty(genId: false, iris: ['https://schema.org/aggregateRating'])]
32+
public AggregateRating $aggregateRating;
33+
34+
#[ApiProperty(property: 'name', iris: ['https://schema.org/name'])]
35+
public string $name;
36+
37+
public static function provide()
38+
{
39+
$s = new self();
40+
$s->code = 'test';
41+
$s->name = 'foo';
42+
$s->aggregateRating = new AggregateRating(1.0, 2);
43+
44+
return $s;
45+
}
46+
}

tests/Functional/JsonStreamerTest.php

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
namespace ApiPlatform\Tests\Functional;
1515

1616
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
17+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AggregateRating;
18+
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Product;
1719
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonStreamResource;
1820
use ApiPlatform\Tests\SetupClassResourcesTrait;
1921
use Doctrine\ORM\EntityManagerInterface;
@@ -30,7 +32,7 @@ class JsonStreamerTest extends ApiTestCase
3032
*/
3133
public static function getResources(): array
3234
{
33-
return [JsonStreamResource::class];
35+
return [JsonStreamResource::class, Product::class, AggregateRating::class];
3436
}
3537

3638
protected function setUp(): void
@@ -299,4 +301,26 @@ public function testJsonStreamerWriteJson(): void
299301
$jsonStreamResource = $manager->find(JsonStreamResource::class, $res['id']);
300302
$this->assertNotNull($jsonStreamResource);
301303
}
304+
305+
public function testJsonStreamerJsonLdGenIdFalseWithDifferentTypeThenShortname(): void
306+
{
307+
$container = static::getContainer();
308+
if ('mongodb' === $container->getParameter('kernel.environment')) {
309+
$this->markTestSkipped();
310+
}
311+
312+
$buffer = '';
313+
ob_start(function (string $chunk) use (&$buffer): void {
314+
$buffer .= $chunk;
315+
});
316+
317+
self::createClient()->request('GET', '/json-stream-products/test', ['headers' => ['accept' => 'application/ld+json']]);
318+
319+
ob_get_clean();
320+
321+
$res = json_decode($buffer, true);
322+
$this->assertArrayNotHasKey('@id', $res['aggregateRating']);
323+
$this->assertEquals('https://schema.org/AggregateRating', $res['aggregateRating']['@type']);
324+
$this->assertEquals('https://schema.org/Product', $res['@type']);
325+
}
302326
}

0 commit comments

Comments
 (0)