Skip to content

Commit 30a9715

Browse files
committed
Allow blank nodes in JSON-LD
1 parent 324df12 commit 30a9715

File tree

5 files changed

+200
-65
lines changed

5 files changed

+200
-65
lines changed

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 5 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@
1616
use ApiPlatform\Core\DataProvider\ItemDataProviderInterface;
1717
use ApiPlatform\Core\Exception\InvalidArgumentException;
1818
use ApiPlatform\Core\Exception\ItemNotFoundException;
19-
use ApiPlatform\Core\Exception\RuntimeException;
2019
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2120
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
21+
use ApiPlatform\Core\Metadata\Util\ItemIdentifiersExtractor;
2222
use ApiPlatform\Core\Util\ClassInfoTrait;
23-
use Symfony\Component\PropertyAccess\PropertyAccess;
2423
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
2524
use Symfony\Component\Routing\Exception\ExceptionInterface as RoutingExceptionInterface;
2625
use Symfony\Component\Routing\RouterInterface;
@@ -34,21 +33,18 @@ final class IriConverter implements IriConverterInterface
3433
{
3534
use ClassInfoTrait;
3635

37-
private $propertyNameCollectionFactory;
38-
private $propertyMetadataFactory;
3936
private $itemDataProvider;
4037
private $routeNameResolver;
4138
private $router;
42-
private $propertyAccessor;
39+
private $itemIdentifiersExtractor;
4340

4441
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, ItemDataProviderInterface $itemDataProvider, RouteNameResolverInterface $routeNameResolver, RouterInterface $router, PropertyAccessorInterface $propertyAccessor = null)
4542
{
46-
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
47-
$this->propertyMetadataFactory = $propertyMetadataFactory;
4843
$this->itemDataProvider = $itemDataProvider;
4944
$this->routeNameResolver = $routeNameResolver;
5045
$this->router = $router;
51-
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
46+
47+
$this->itemIdentifiersExtractor = new ItemIdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor);
5248
}
5349

5450
/**
@@ -81,7 +77,7 @@ public function getIriFromItem($item, int $referenceType = UrlGeneratorInterface
8177
$resourceClass = $this->getObjectClass($item);
8278
$routeName = $this->routeNameResolver->getRouteName($resourceClass, false);
8379

84-
$identifiers = $this->generateIdentifiersUrl($this->getIdentifiersFromItem($item));
80+
$identifiers = $this->generateIdentifiersUrl($this->itemIdentifiersExtractor->getIdentifiersFromItem($item));
8581

8682
return $this->router->generate($routeName, ['id' => implode(';', $identifiers)], $referenceType);
8783
}
@@ -98,59 +94,6 @@ public function getIriFromResourceClass(string $resourceClass, int $referenceTyp
9894
}
9995
}
10096

101-
/**
102-
* Find identifiers from an Item (Object).
103-
*
104-
* @param object $item
105-
*
106-
* @throws RuntimeException
107-
*
108-
* @return array
109-
*/
110-
private function getIdentifiersFromItem($item): array
111-
{
112-
$identifiers = [];
113-
$resourceClass = $this->getObjectClass($item);
114-
115-
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
116-
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
117-
118-
$identifier = $propertyMetadata->isIdentifier();
119-
if (null === $identifier || false === $identifier) {
120-
continue;
121-
}
122-
123-
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
124-
125-
if (!is_object($identifiers[$propertyName])) {
126-
continue;
127-
}
128-
129-
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
130-
$relatedItem = $identifiers[$propertyName];
131-
132-
unset($identifiers[$propertyName]);
133-
134-
foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) {
135-
$propertyMetadata = $this->propertyMetadataFactory->create($relatedResourceClass, $relatedPropertyName);
136-
137-
if ($propertyMetadata->isIdentifier()) {
138-
if (isset($identifiers[$propertyName])) {
139-
throw new RuntimeException(sprintf('Composite identifiers not supported in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
140-
}
141-
142-
$identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedPropertyName);
143-
}
144-
}
145-
146-
if (!isset($identifiers[$propertyName])) {
147-
throw new RuntimeException(sprintf('No identifier found in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
148-
}
149-
}
150-
151-
return $identifiers;
152-
}
153-
15497
/**
15598
* Generate the identifier url.
15699
*

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@
1515
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1616
use ApiPlatform\Core\Exception\InvalidArgumentException;
1717
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
18+
use ApiPlatform\Core\JsonLd\Util\BlankNodeIdentifiersGenerator;
1819
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
1920
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
21+
use ApiPlatform\Core\Metadata\Property\PropertyMetadata;
2022
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
23+
use ApiPlatform\Core\Metadata\Util\ItemIdentifiersExtractor;
2124
use ApiPlatform\Core\Serializer\AbstractItemNormalizer;
2225
use ApiPlatform\Core\Serializer\ContextTrait;
2326
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
@@ -38,13 +41,18 @@ final class ItemNormalizer extends AbstractItemNormalizer
3841

3942
private $resourceMetadataFactory;
4043
private $contextBuilder;
44+
private $itemIdentifiersExtractor;
45+
private $blankNodeIdentifiersGenerator;
4146

4247
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null)
4348
{
4449
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory);
4550

4651
$this->resourceMetadataFactory = $resourceMetadataFactory;
4752
$this->contextBuilder = $contextBuilder;
53+
54+
$this->itemIdentifiersExtractor = new ItemIdentifiersExtractor($propertyNameCollectionFactory, $propertyMetadataFactory, $propertyAccessor);
55+
$this->blankNodeIdentifiersGenerator = new BlankNodeIdentifiersGenerator();
4856
}
4957

5058
/**
@@ -64,12 +72,16 @@ public function normalize($object, $format = null, array $context = [])
6472
$resourceMetadata = $this->resourceMetadataFactory->create($resourceClass);
6573
$data = $this->addJsonLdContext($this->contextBuilder, $resourceClass, $context);
6674

75+
$context = $this->addJsonLdDocumentContext($object, $context);
76+
6777
$rawData = parent::normalize($object, $format, $context);
6878
if (!is_array($rawData)) {
6979
return $rawData;
7080
}
7181

72-
$data['@id'] = $this->iriConverter->getIriFromItem($object);
82+
$identifiers = $this->itemIdentifiersExtractor->getIdentifiersFromItem($object);
83+
84+
$data['@id'] = count(array_filter($identifiers)) > 0 ? $this->iriConverter->getIriFromItem($relatedObject) : $this->blankNodeIdentifiersGenerator->getBlankNodeIdentifier($relatedObject, $context['jsonld_document_root']);
7385
$data['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName();
7486

7587
return $data + $rawData;
@@ -90,6 +102,12 @@ public function supportsDenormalization($data, $type, $format = null)
90102
*/
91103
public function denormalize($data, $class, $format = null, array $context = [])
92104
{
105+
// Blank node identifiers cannot be used in denormalization
106+
// Denormalize into new object
107+
if (isset($data['@id']) && '_:' === substr($data['@id'], 0, 2)) {
108+
unset($data['@id']);
109+
}
110+
93111
// Avoid issues with proxies if we populated the object
94112
if (isset($data['@id']) && !isset($context['object_to_populate'])) {
95113
if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
@@ -101,4 +119,33 @@ public function denormalize($data, $class, $format = null, array $context = [])
101119

102120
return parent::denormalize($data, $class, $format, $context);
103121
}
122+
123+
/**
124+
* {@inheritdoc}
125+
*/
126+
protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
127+
{
128+
if ($propertyMetadata->isReadableLink()) {
129+
return $this->serializer->normalize($relatedObject, $format, $this->createRelationSerializationContext($resourceClass, $context));
130+
}
131+
132+
$identifiers = $this->itemIdentifiersExtractor->getIdentifiersFromItem($relatedObject);
133+
134+
return count(array_filter($identifiers)) > 0 ? $this->iriConverter->getIriFromItem($relatedObject) : $this->blankNodeIdentifiersGenerator->getBlankNodeIdentifier($relatedObject, $context['jsonld_document_root']);
135+
}
136+
137+
/**
138+
* Adds information related to the JSON-LD document to the serializer context.
139+
*
140+
* @param object $object
141+
* @param array $context
142+
*
143+
* @return array
144+
*/
145+
private function addJsonLdDocumentContext($object, array $context)
146+
{
147+
$context['jsonld_document_root'] ?? $context['jsonld_document_root'] = spl_object_hash($object);
148+
149+
return $context;
150+
}
104151
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
namespace ApiPlatform\Core\JsonLd\Util;
13+
14+
/**
15+
* Generates blank node identifiers scoped to each JSON-LD document.
16+
*
17+
* @author Teoh Han Hui <teohhanhui@gmail.com>
18+
*
19+
* @internal
20+
*/
21+
final class BlankNodeIdentifiersGenerator
22+
{
23+
const IDENTIFIER_PREFIX = '_:b';
24+
25+
private $blankNodeCounts = [];
26+
private $identifiers = [];
27+
28+
/**
29+
* Gets a blank node identifier for an object, scoped to a JSON-LD document.
30+
*
31+
* @param object $object
32+
* @param string $documentRootHash
33+
*
34+
* @return string
35+
*/
36+
public function getBlankNodeIdentifier($object, string $documentRootHash): string
37+
{
38+
$objectHash = spl_object_hash($object);
39+
40+
if (!isset($this->identifiers[$documentRootHash][$objectHash])) {
41+
$this->blankNodeCounts[$documentRootHash] ?? $this->blankNodeCounts[$documentRootHash] = 0;
42+
$this->identifiers[$documentRootHash] ?? $this->identifiers[$documentRootHash] = [];
43+
44+
$this->identifiers[$documentRootHash][$objectHash] = sprintf('%s%d', self::IDENTIFIER_PREFIX, $this->blankNodeCounts[$documentRootHash]++);
45+
}
46+
47+
return $this->identifiers[$documentRootHash][$objectHash];
48+
}
49+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
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+
namespace ApiPlatform\Core\Metadata\Util;
13+
14+
use ApiPlatform\Core\Exception\RuntimeException;
15+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
16+
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
17+
use ApiPlatform\Core\Util\ClassInfoTrait;
18+
use Symfony\Component\PropertyAccess\PropertyAccess;
19+
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
20+
21+
/**
22+
* Extracts identifiers from an item.
23+
*
24+
* @author Amrouche Hamza <hamza.simperfit@gmail.com>
25+
* @author Teoh Han Hui <teohhanhui@gmail.com>
26+
*
27+
* @internal
28+
*/
29+
final class ItemIdentifiersExtractor
30+
{
31+
use ClassInfoTrait;
32+
33+
private $propertyNameCollectionFactory;
34+
private $propertyMetadataFactory;
35+
private $propertyAccessor;
36+
37+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, PropertyAccessorInterface $propertyAccessor = null)
38+
{
39+
$this->propertyNameCollectionFactory = $propertyNameCollectionFactory;
40+
$this->propertyMetadataFactory = $propertyMetadataFactory;
41+
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
42+
}
43+
44+
/**
45+
* Gets the identifiers from an item.
46+
*
47+
* @param object $item
48+
*
49+
* @throws RuntimeException
50+
*
51+
* @return array
52+
*/
53+
public function getIdentifiersFromItem($item): array
54+
{
55+
$identifiers = [];
56+
$resourceClass = $this->getObjectClass($item);
57+
58+
foreach ($this->propertyNameCollectionFactory->create($resourceClass) as $propertyName) {
59+
$propertyMetadata = $this->propertyMetadataFactory->create($resourceClass, $propertyName);
60+
61+
$identifier = $propertyMetadata->isIdentifier();
62+
if (null === $identifier || false === $identifier) {
63+
continue;
64+
}
65+
66+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($item, $propertyName);
67+
68+
if (!is_object($identifiers[$propertyName])) {
69+
continue;
70+
}
71+
72+
$relatedResourceClass = $this->getObjectClass($identifiers[$propertyName]);
73+
$relatedItem = $identifiers[$propertyName];
74+
75+
unset($identifiers[$propertyName]);
76+
77+
foreach ($this->propertyNameCollectionFactory->create($relatedResourceClass) as $relatedPropertyName) {
78+
$propertyMetadata = $this->propertyMetadataFactory->create($relatedResourceClass, $relatedPropertyName);
79+
80+
if ($propertyMetadata->isIdentifier()) {
81+
if (isset($identifiers[$propertyName])) {
82+
throw new RuntimeException(sprintf('Composite identifiers not supported in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
83+
}
84+
85+
$identifiers[$propertyName] = $this->propertyAccessor->getValue($relatedItem, $relatedPropertyName);
86+
}
87+
}
88+
89+
if (!isset($identifiers[$propertyName])) {
90+
throw new RuntimeException(sprintf('No identifier found in "%s" through relation "%s" of "%s" used as identifier', $relatedResourceClass, $propertyName, $resourceClass));
91+
}
92+
}
93+
94+
return $identifiers;
95+
}
96+
}

src/Serializer/AbstractItemNormalizer.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -405,7 +405,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
405405
}
406406

407407
/**
408-
* Normalizes a relation as an URI if is a Link or as a JSON-LD object.
408+
* Normalizes a relation.
409409
*
410410
* @param PropertyMetadata $propertyMetadata
411411
* @param mixed $relatedObject
@@ -415,7 +415,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
415415
*
416416
* @return string|array
417417
*/
418-
private function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
418+
protected function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
419419
{
420420
if ($propertyMetadata->isReadableLink()) {
421421
return $this->serializer->normalize($relatedObject, $format, $this->createRelationSerializationContext($resourceClass, $context));

0 commit comments

Comments
 (0)