diff --git a/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php b/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php
index aed1098b8fe..238a19d72b4 100644
--- a/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php
+++ b/src/JsonLd/JsonStreamer/ValueTransformer/TypeValueTransformer.php
@@ -15,26 +15,56 @@
use ApiPlatform\Hydra\Collection;
use ApiPlatform\Metadata\Exception\RuntimeException;
+use ApiPlatform\Metadata\HttpOperation;
+use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface;
+use ApiPlatform\Metadata\ResourceClassResolverInterface;
use Symfony\Component\JsonStreamer\ValueTransformer\ValueTransformerInterface;
use Symfony\Component\TypeInfo\Type;
final class TypeValueTransformer implements ValueTransformerInterface
{
+ public function __construct(
+ private readonly ResourceClassResolverInterface $resourceClassResolver,
+ private readonly ResourceMetadataCollectionFactoryInterface $resourceMetadataCollectionFactory,
+ ) {
+ }
+
public function transform(mixed $value, array $options = []): mixed
{
if ($options['_current_object'] instanceof Collection) {
return 'Collection';
}
- if (!isset($options['operation'])) {
- throw new RuntimeException('Operation is not defined');
+ $dataClass = isset($options['data']) && \is_object($options['data']) ? $options['data']::class : null;
+ if (($currentClass = $options['_current_object']::class) === $dataClass) {
+ if (!isset($options['operation'])) {
+ throw new RuntimeException('Operation is not defined');
+ }
+
+ return $this->getOperationType($options['operation']);
+ }
+
+ if (!$this->resourceClassResolver->isResourceClass($currentClass)) {
+ return null;
}
- return $options['operation']->getShortName();
+ /** @var HttpOperation $op */
+ $op = $this->resourceMetadataCollectionFactory->create($currentClass)->getOperation(httpOperation: true);
+
+ return $this->getOperationType($op);
}
public static function getStreamValueType(): Type
{
return Type::string();
}
+
+ private function getOperationType(HttpOperation $operation): array|string
+ {
+ if (($t = $operation->getTypes()) && 1 === \count($t)) {
+ return $operation->getTypes()[0];
+ }
+
+ return $t ?: $operation->getShortname();
+ }
}
diff --git a/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php b/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php
index a3cba6c0246..147be2ee14d 100644
--- a/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php
+++ b/src/JsonLd/JsonStreamer/WritePropertyMetadataLoader.php
@@ -15,6 +15,8 @@
use ApiPlatform\Hydra\Collection;
use ApiPlatform\Hydra\IriTemplate;
+use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
+use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
use ApiPlatform\Metadata\ResourceClassResolverInterface;
use ApiPlatform\Metadata\Util\TypeHelper;
use Symfony\Component\JsonStreamer\Mapping\PropertyMetadata;
@@ -26,6 +28,8 @@ final class WritePropertyMetadataLoader implements PropertyMetadataLoaderInterfa
public function __construct(
private readonly PropertyMetadataLoaderInterface $loader,
private readonly ResourceClassResolverInterface $resourceClassResolver,
+ private readonly PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory,
+ private readonly PropertyMetadataFactoryInterface $propertyMetadataFactory,
) {
}
@@ -47,23 +51,47 @@ public function load(string $className, array $options = [], array $context = []
return $properties;
}
- $properties['@id'] = new PropertyMetadata(
- 'id', // virtual property
- Type::mixed(), // virtual property
- ['api_platform.jsonld.json_streamer.write.value_transformer.iri'],
- );
+ $originalClassName = TypeHelper::getClassName($context['original_type']);
+ $hasIri = true;
+ $virtualProperty = 'id';
+
+ foreach ($this->propertyNameCollectionFactory->create($originalClassName) as $property) {
+ $propertyMetadata = $this->propertyMetadataFactory->create($originalClassName, $property);
+ if ($propertyMetadata->isIdentifier()) {
+ $virtualProperty = $property;
+ }
+
+ if ($className === $originalClassName) {
+ continue;
+ }
+
+ if ($propertyMetadata->getNativeType()->isIdentifiedBy($className)) {
+ $hasIri = $propertyMetadata->getGenId();
+ $virtualProperty = iterator_to_array($this->propertyNameCollectionFactory->create($className))[0];
+ }
+ }
+
+ if ($hasIri) {
+ $properties['@id'] = new PropertyMetadata(
+ $virtualProperty, // virtual property
+ Type::mixed(), // virtual property
+ ['api_platform.jsonld.json_streamer.write.value_transformer.iri'],
+ );
+ }
$properties['@type'] = new PropertyMetadata(
- 'id', // virtual property
+ $virtualProperty, // virtual property
Type::mixed(), // virtual property
['api_platform.jsonld.json_streamer.write.value_transformer.type'],
);
- $originalClassName = TypeHelper::getClassName($context['original_type']);
+ if ($className !== $originalClassName) {
+ return $properties;
+ }
if (Collection::class === $originalClassName || ($this->resourceClassResolver->isResourceClass($originalClassName) && !isset($context['generated_classes'][Collection::class]))) {
$properties['@context'] = new PropertyMetadata(
- 'id', // virual property
+ $virtualProperty, // virual property
Type::string(), // virtual property
['api_platform.jsonld.json_streamer.write.value_transformer.context'],
);
diff --git a/src/Symfony/Bundle/Resources/config/json_streamer/common.xml b/src/Symfony/Bundle/Resources/config/json_streamer/common.xml
index 38fe492a19d..dfa97ccb8f7 100644
--- a/src/Symfony/Bundle/Resources/config/json_streamer/common.xml
+++ b/src/Symfony/Bundle/Resources/config/json_streamer/common.xml
@@ -19,6 +19,8 @@
+
+
@@ -27,6 +29,8 @@
+
+
diff --git a/tests/Fixtures/TestBundle/ApiResource/AggregateRating.php b/tests/Fixtures/TestBundle/ApiResource/AggregateRating.php
new file mode 100644
index 00000000000..00f69b990c4
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/AggregateRating.php
@@ -0,0 +1,29 @@
+
+ *
+ * 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\ApiResource;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\ApiResource;
+
+#[ApiResource(types: 'https://schema.org/AggregateRating', operations: [])]
+final class AggregateRating
+{
+ public function __construct(
+ #[ApiProperty(iris: ['https://schema.org/ratingValue'])]
+ public float $ratingValue,
+ #[ApiProperty(iris: ['https://schema.org/reviewCount'])]
+ public int $reviewCount,
+ ) {
+ }
+}
diff --git a/tests/Fixtures/TestBundle/ApiResource/Product.php b/tests/Fixtures/TestBundle/ApiResource/Product.php
new file mode 100644
index 00000000000..de231d0ea58
--- /dev/null
+++ b/tests/Fixtures/TestBundle/ApiResource/Product.php
@@ -0,0 +1,46 @@
+
+ *
+ * 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\ApiResource;
+
+use ApiPlatform\Metadata\ApiProperty;
+use ApiPlatform\Metadata\Get;
+
+#[Get(
+ types: ['https://schema.org/Product'],
+ uriTemplate: '/json-stream-products/{code}',
+ uriVariables: ['code'],
+ provider: [self::class, 'provide'],
+ jsonStream: true
+)]
+class Product
+{
+ #[ApiProperty(identifier: true)]
+ public string $code;
+
+ #[ApiProperty(genId: false, iris: ['https://schema.org/aggregateRating'])]
+ public AggregateRating $aggregateRating;
+
+ #[ApiProperty(property: 'name', iris: ['https://schema.org/name'])]
+ public string $name;
+
+ public static function provide()
+ {
+ $s = new self();
+ $s->code = 'test';
+ $s->name = 'foo';
+ $s->aggregateRating = new AggregateRating(1.0, 2);
+
+ return $s;
+ }
+}
diff --git a/tests/Functional/JsonStreamerTest.php b/tests/Functional/JsonStreamerTest.php
index 3760e707503..bf5d49b436f 100644
--- a/tests/Functional/JsonStreamerTest.php
+++ b/tests/Functional/JsonStreamerTest.php
@@ -14,10 +14,14 @@
namespace ApiPlatform\Tests\Functional;
use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\AggregateRating;
+use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Product;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\JsonStreamResource;
use ApiPlatform\Tests\SetupClassResourcesTrait;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Tools\SchemaTool;
+use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
+use Symfony\Component\JsonStreamer\JsonStreamWriter;
class JsonStreamerTest extends ApiTestCase
{
@@ -30,7 +34,7 @@ class JsonStreamerTest extends ApiTestCase
*/
public static function getResources(): array
{
- return [JsonStreamResource::class];
+ return [JsonStreamResource::class, Product::class, AggregateRating::class];
}
protected function setUp(): void
@@ -98,6 +102,10 @@ protected function tearDown(): void
public function testJsonStreamerJsonLd(): void
{
$container = static::getContainer();
+ if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
+ $this->markTestSkipped('JsonStreamer component not installed.');
+ }
+
if ('mongodb' === $container->getParameter('kernel.environment')) {
$this->markTestSkipped();
}
@@ -123,6 +131,9 @@ public function testJsonStreamerJsonLd(): void
public function testJsonStreamerCollectionJsonLd(): void
{
+ if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
+ $this->markTestSkipped('JsonStreamer component not installed.');
+ }
$container = static::getContainer();
if ('mongodb' === $container->getParameter('kernel.environment')) {
$this->markTestSkipped();
@@ -153,6 +164,9 @@ public function testJsonStreamerCollectionJsonLd(): void
public function testJsonStreamerJson(): void
{
+ if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
+ $this->markTestSkipped('JsonStreamer component not installed.');
+ }
$container = static::getContainer();
if ('mongodb' === $container->getParameter('kernel.environment')) {
$this->markTestSkipped();
@@ -179,6 +193,9 @@ public function testJsonStreamerJson(): void
public function testJsonStreamerCollectionJson(): void
{
+ if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
+ $this->markTestSkipped('JsonStreamer component not installed.');
+ }
$container = static::getContainer();
if ('mongodb' === $container->getParameter('kernel.environment')) {
$this->markTestSkipped();
@@ -203,6 +220,9 @@ public function testJsonStreamerCollectionJson(): void
public function testJsonStreamerWriteJsonLd(): void
{
+ if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
+ $this->markTestSkipped('JsonStreamer component not installed.');
+ }
$container = static::getContainer();
if ('mongodb' === $container->getParameter('kernel.environment')) {
$this->markTestSkipped();
@@ -299,4 +319,29 @@ public function testJsonStreamerWriteJson(): void
$jsonStreamResource = $manager->find(JsonStreamResource::class, $res['id']);
$this->assertNotNull($jsonStreamResource);
}
+
+ public function testJsonStreamerJsonLdGenIdFalseWithDifferentTypeThenShortname(): void
+ {
+ if (false === (class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class))) {
+ $this->markTestSkipped('JsonStreamer component not installed.');
+ }
+ $container = static::getContainer();
+ if ('mongodb' === $container->getParameter('kernel.environment')) {
+ $this->markTestSkipped();
+ }
+
+ $buffer = '';
+ ob_start(function (string $chunk) use (&$buffer): void {
+ $buffer .= $chunk;
+ });
+
+ self::createClient()->request('GET', '/json-stream-products/test', ['headers' => ['accept' => 'application/ld+json']]);
+
+ ob_get_clean();
+
+ $res = json_decode($buffer, true);
+ $this->assertArrayNotHasKey('@id', $res['aggregateRating']);
+ $this->assertEquals('https://schema.org/AggregateRating', $res['aggregateRating']['@type']);
+ $this->assertEquals('https://schema.org/Product', $res['@type']);
+ }
}
diff --git a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
index 000d321c2d9..76264c21278 100644
--- a/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
+++ b/tests/Symfony/Bundle/DependencyInjection/ConfigurationTest.php
@@ -17,6 +17,7 @@
use ApiPlatform\Symfony\Bundle\DependencyInjection\Configuration;
use Doctrine\ORM\OptimisticLockException;
use PHPUnit\Framework\TestCase;
+use Symfony\Bundle\FrameworkBundle\Controller\ControllerHelper;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
@@ -74,7 +75,7 @@ private function runDefaultConfigTests(array $doctrineIntegrationsToLoad = ['orm
$this->assertEquals([
'title' => 'title',
'description' => 'description',
- 'enable_json_streamer' => class_exists(JsonStreamWriter::class),
+ 'enable_json_streamer' => class_exists(ControllerHelper::class) && class_exists(JsonStreamWriter::class),
'version' => '1.0.0',
'show_webby' => true,
'formats' => [