Skip to content

Commit 5a8d4d2

Browse files
authored
fix(jsonld): various json streamer fixes (#7374)
1 parent 6db55be commit 5a8d4d2

File tree

7 files changed

+196
-13
lines changed

7 files changed

+196
-13
lines changed

src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,26 +15,56 @@
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 = isset($options['data']) && \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+
/** @var HttpOperation $op */
52+
$op = $this->resourceMetadataCollectionFactory->create($currentClass)->getOperation(httpOperation: true);
53+
54+
return $this->getOperationType($op);
3455
}
3556

3657
public static function getStreamValueType(): Type
3758
{
3859
return Type::string();
3960
}
61+
62+
private function getOperationType(HttpOperation $operation): array|string
63+
{
64+
if (($t = $operation->getTypes()) && 1 === \count($t)) {
65+
return $operation->getTypes()[0];
66+
}
67+
68+
return $t ?: $operation->getShortname();
69+
}
4070
}

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 <dunglas@gmail.com>
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 <dunglas@gmail.com>
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: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@
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;
2022
use Doctrine\ORM\Tools\SchemaTool;
23+
use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
24+
use Symfony\Component\JsonStreamer\JsonStreamWriter;
2125

2226
class JsonStreamerTest extends ApiTestCase
2327
{
@@ -30,7 +34,7 @@ class JsonStreamerTest extends ApiTestCase
3034
*/
3135
public static function getResources(): array
3236
{
33-
return [JsonStreamResource::class];
37+
return [JsonStreamResource::class, Product::class, AggregateRating::class];
3438
}
3539

3640
protected function setUp(): void
@@ -98,6 +102,10 @@ protected function tearDown(): void
98102
public function testJsonStreamerJsonLd(): void
99103
{
100104
$container = static::getContainer();
105+
if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
106+
$this->markTestSkipped('JsonStreamer component not installed.');
107+
}
108+
101109
if ('mongodb' === $container->getParameter('kernel.environment')) {
102110
$this->markTestSkipped();
103111
}
@@ -123,6 +131,9 @@ public function testJsonStreamerJsonLd(): void
123131

124132
public function testJsonStreamerCollectionJsonLd(): void
125133
{
134+
if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
135+
$this->markTestSkipped('JsonStreamer component not installed.');
136+
}
126137
$container = static::getContainer();
127138
if ('mongodb' === $container->getParameter('kernel.environment')) {
128139
$this->markTestSkipped();
@@ -153,6 +164,9 @@ public function testJsonStreamerCollectionJsonLd(): void
153164

154165
public function testJsonStreamerJson(): void
155166
{
167+
if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
168+
$this->markTestSkipped('JsonStreamer component not installed.');
169+
}
156170
$container = static::getContainer();
157171
if ('mongodb' === $container->getParameter('kernel.environment')) {
158172
$this->markTestSkipped();
@@ -179,6 +193,9 @@ public function testJsonStreamerJson(): void
179193

180194
public function testJsonStreamerCollectionJson(): void
181195
{
196+
if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
197+
$this->markTestSkipped('JsonStreamer component not installed.');
198+
}
182199
$container = static::getContainer();
183200
if ('mongodb' === $container->getParameter('kernel.environment')) {
184201
$this->markTestSkipped();
@@ -203,6 +220,9 @@ public function testJsonStreamerCollectionJson(): void
203220

204221
public function testJsonStreamerWriteJsonLd(): void
205222
{
223+
if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
224+
$this->markTestSkipped('JsonStreamer component not installed.');
225+
}
206226
$container = static::getContainer();
207227
if ('mongodb' === $container->getParameter('kernel.environment')) {
208228
$this->markTestSkipped();
@@ -299,4 +319,29 @@ public function testJsonStreamerWriteJson(): void
299319
$jsonStreamResource = $manager->find(JsonStreamResource::class, $res['id']);
300320
$this->assertNotNull($jsonStreamResource);
301321
}
322+
323+
public function testJsonStreamerJsonLdGenIdFalseWithDifferentTypeThenShortname(): void
324+
{
325+
if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
326+
$this->markTestSkipped('JsonStreamer component not installed.');
327+
}
328+
$container = static::getContainer();
329+
if ('mongodb' === $container->getParameter('kernel.environment')) {
330+
$this->markTestSkipped();
331+
}
332+
333+
$buffer = '';
334+
ob_start(function (string $chunk) use (&$buffer): void {
335+
$buffer .= $chunk;
336+
});
337+
338+
self::createClient()->request('GET', '/json-stream-products/test', ['headers' => ['accept' => 'application/ld+json']]);
339+
340+
ob_get_clean();
341+
342+
$res = json_decode($buffer, true);
343+
$this->assertArrayNotHasKey('@id', $res['aggregateRating']);
344+
$this->assertEquals('https://schema.org/AggregateRating', $res['aggregateRating']['@type']);
345+
$this->assertEquals('https://schema.org/Product', $res['@type']);
346+
}
302347
}

tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use ApiPlatform\Symfony\Bundle\DependencyInjection\Configuration;
1818
use Doctrine\ORM\OptimisticLockException;
1919
use PHPUnit\Framework\TestCase;
20+
use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
2021
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
2122
use Symfony\Component\Config\Definition\ConfigurationInterface;
2223
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
@@ -74,7 +75,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
7475
$this->assertEquals([
7576
'title' => 'title',
7677
'description' => 'description',
77-
'enable_json_streamer' => class_exists(JsonStreamWriter::class),
78+
'enable_json_streamer' => class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class),
7879
'version' => '1.0.0',
7980
'show_webby' => true,
8081
'formats' => [

0 commit comments

Comments
 (0)