Skip to content

Commit 4b2d52b

Browse files
authored
Merge 4c6d144 into 8967ce9
2 parents 8967ce9 + 4c6d144 commit 4b2d52b

File tree

11 files changed

+305
-49
lines changed

11 files changed

+305
-49
lines changed

src/Bridge/Symfony/Bundle/Resources/config/api.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
<argument type="service" id="api_platform.property_accessor" />
8080
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
8181
<argument type="service" id="serializer.mapping.class_metadata_factory" on-invalid="ignore" />
82+
<argument type="service" id="api_platform.identifiers_extractor.cached" />
8283

8384
<tag name="serializer.normalizer" />
8485
</service>

src/Bridge/Symfony/Bundle/Resources/config/hal.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<argument type="service" id="api_platform.property_accessor" />
3535
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
3636
<argument type="service" id="serializer.mapping.class_metadata_factory" on-invalid="ignore" />
37+
<argument type="service" id="api_platform.identifiers_extractor.cached" />
3738

3839
<tag name="serializer.normalizer" priority="8" />
3940
</service>

src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
<argument type="service" id="api_platform.property_accessor" />
2626
<argument type="service" id="api_platform.name_converter" on-invalid="ignore" />
2727
<argument type="service" id="serializer.mapping.class_metadata_factory" on-invalid="ignore" />
28+
<argument type="service" id="api_platform.identifiers_extractor.cached" />
2829

2930
<tag name="serializer.normalizer" priority="8" />
3031
</service>

src/Bridge/Symfony/Routing/IriConverter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
5757
$this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor();
5858

5959
if (null === $identifiersExtractor) {
60-
@trigger_error('Not injecting ItemIdentifiersExtractor is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3');
60+
@trigger_error('Not injecting IdentifiersExtractorInterface is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3');
6161
$this->identifiersExtractor = new IdentifiersExtractor($this->propertyNameCollectionFactory, $this->propertyMetadataFactory, $this->propertyAccessor);
6262
} else {
6363
$this->identifiersExtractor = $identifiersExtractor;

src/JsonLd/Serializer/ItemNormalizer.php

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,12 @@
1313

1414
namespace ApiPlatform\Core\JsonLd\Serializer;
1515

16+
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1617
use ApiPlatform\Core\Api\IriConverterInterface;
1718
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1819
use ApiPlatform\Core\Exception\InvalidArgumentException;
1920
use ApiPlatform\Core\JsonLd\ContextBuilderInterface;
21+
use ApiPlatform\Core\JsonLd\Util\BlankNodeIdentifiersGenerator;
2022
use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface;
2123
use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface;
2224
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
@@ -40,13 +42,16 @@ final class ItemNormalizer extends AbstractItemNormalizer
4042

4143
private $resourceMetadataFactory;
4244
private $contextBuilder;
45+
private $blankNodeIdentifiersGenerator;
4346

44-
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null)
47+
public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, IdentifiersExtractorInterface $identifiersExtractor = null)
4548
{
46-
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory);
49+
parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $identifiersExtractor);
4750

4851
$this->resourceMetadataFactory = $resourceMetadataFactory;
4952
$this->contextBuilder = $contextBuilder;
53+
54+
$this->blankNodeIdentifiersGenerator = new BlankNodeIdentifiersGenerator();
5055
}
5156

5257
/**
@@ -68,14 +73,20 @@ public function normalize($object, $format = null, array $context = [])
6873

6974
// Use resolved resource class instead of given resource class to support multiple inheritance child types
7075
$context['resource_class'] = $resourceClass;
71-
$context['iri'] = $this->iriConverter->getIriFromItem($object);
76+
77+
$context = $this->addJsonLdDocumentContext($object, $context);
78+
79+
$jsonLdIdentifier = $this->getJsonLdNodeIdentifier($object, $context);
80+
if ($this->hasIri($object)) {
81+
$context['iri'] = $jsonLdIdentifier;
82+
}
7283

7384
$rawData = parent::normalize($object, $format, $context);
7485
if (!is_array($rawData)) {
7586
return $rawData;
7687
}
7788

78-
$data['@id'] = $context['iri'];
89+
$data['@id'] = $jsonLdIdentifier;
7990
$data['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName();
8091

8192
return $data + $rawData;
@@ -96,6 +107,12 @@ public function supportsDenormalization($data, $type, $format = null)
96107
*/
97108
public function denormalize($data, $class, $format = null, array $context = [])
98109
{
110+
// Blank node identifiers cannot be used in denormalization
111+
// Denormalize into new object
112+
if (isset($data['@id']) && $this->isBlankNodeIdentifier($data['@id'])) {
113+
unset($data['@id']);
114+
}
115+
99116
// Avoid issues with proxies if we populated the object
100117
if (isset($data['@id']) && !isset($context['object_to_populate'])) {
101118
if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) {
@@ -107,4 +124,44 @@ public function denormalize($data, $class, $format = null, array $context = [])
107124

108125
return parent::denormalize($data, $class, $format, $context);
109126
}
127+
128+
/**
129+
* Adds information related to the JSON-LD document to the serializer context.
130+
*
131+
* @param object $object
132+
* @param array $context
133+
*
134+
* @return array
135+
*/
136+
private function addJsonLdDocumentContext($object, array $context)
137+
{
138+
$context['jsonld_document_root'] ?? $context['jsonld_document_root'] = spl_object_hash($object);
139+
140+
return $context;
141+
}
142+
143+
/**
144+
* Gets the identifier for a JSON-LD node.
145+
*
146+
* @param object $object
147+
* @param array $context
148+
*
149+
* @return string
150+
*/
151+
private function getJsonLdNodeIdentifier($object, array $context): string
152+
{
153+
return $this->hasIri($object) ? $this->iriConverter->getIriFromItem($object) : $this->blankNodeIdentifiersGenerator->getBlankNodeIdentifier($object, $context['jsonld_document_root']);
154+
}
155+
156+
/**
157+
* Determines whether an IRI is a JSON-LD blank node identifier.
158+
*
159+
* @param string $iri
160+
*
161+
* @return bool
162+
*/
163+
private function isBlankNodeIdentifier(string $iri): bool
164+
{
165+
return '_:' === substr($iri, 0, 2);
166+
}
110167
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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\Core\JsonLd\Util;
15+
16+
/**
17+
* Generates blank node identifiers scoped to each JSON-LD document.
18+
*
19+
* @author Teoh Han Hui <teohhanhui@gmail.com>
20+
*
21+
* @internal
22+
*/
23+
final class BlankNodeIdentifiersGenerator
24+
{
25+
const IDENTIFIER_PREFIX = '_:b';
26+
27+
private $blankNodeCounts = [];
28+
private $identifiers = [];
29+
30+
/**
31+
* Gets a blank node identifier for an object, scoped to a JSON-LD document.
32+
*
33+
* @param object $object
34+
* @param string $documentRootHash
35+
*
36+
* @return string
37+
*/
38+
public function getBlankNodeIdentifier($object, string $documentRootHash): string
39+
{
40+
$objectHash = spl_object_hash($object);
41+
42+
if (!isset($this->identifiers[$documentRootHash][$objectHash])) {
43+
$this->blankNodeCounts[$documentRootHash] ?? $this->blankNodeCounts[$documentRootHash] = 0;
44+
$this->identifiers[$documentRootHash] ?? $this->identifiers[$documentRootHash] = [];
45+
46+
$this->identifiers[$documentRootHash][$objectHash] = sprintf('%s%d', self::IDENTIFIER_PREFIX, $this->blankNodeCounts[$documentRootHash]++);
47+
}
48+
49+
return $this->identifiers[$documentRootHash][$objectHash];
50+
}
51+
}

src/Serializer/AbstractItemNormalizer.php

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313

1414
namespace ApiPlatform\Core\Serializer;
1515

16+
use ApiPlatform\Core\Api\IdentifiersExtractor;
17+
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1618
use ApiPlatform\Core\Api\IriConverterInterface;
1719
use ApiPlatform\Core\Api\OperationType;
1820
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
@@ -43,8 +45,9 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer
4345
protected $iriConverter;
4446
protected $resourceClassResolver;
4547
protected $propertyAccessor;
48+
protected $identifiersExtractor;
4649

47-
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null)
50+
public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, IdentifiersExtractorInterface $identifiersExtractor = null)
4851
{
4952
parent::__construct($classMetadataFactory, $nameConverter);
5053

@@ -54,8 +57,15 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName
5457
$this->resourceClassResolver = $resourceClassResolver;
5558
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
5659

60+
if (null === $identifiersExtractor) {
61+
@trigger_error('Not injecting IdentifiersExtractorInterface is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3');
62+
$this->identifiersExtractor = new IdentifiersExtractor($this->propertyNameCollectionFactory, $this->propertyMetadataFactory, $this->propertyAccessor);
63+
} else {
64+
$this->identifiersExtractor = $identifiersExtractor;
65+
}
66+
5767
$this->setCircularReferenceHandler(function ($object) {
58-
return $this->iriConverter->getIriFromItem($object);
68+
return $this->hasIri($object) ? $this->iriConverter->getIriFromItem($object) : spl_object_hash($object);
5969
});
6070
}
6171

@@ -86,7 +96,7 @@ public function normalize($object, $format = null, array $context = [])
8696
$context = $this->initContext($resourceClass, $context);
8797
$context['api_normalize'] = true;
8898

89-
if (isset($context['resources'])) {
99+
if (isset($context['resources']) && $this->hasIri($object)) {
90100
$resource = $context['iri'] ?? $this->iriConverter->getIriFromItem($object);
91101
$context['resources'][$resource] = $resource;
92102
}
@@ -421,7 +431,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
421431
}
422432

423433
/**
424-
* Normalizes a relation as an URI if is a Link or as a JSON-LD object.
434+
* Normalizes a relation.
425435
*
426436
* @param PropertyMetadata $propertyMetadata
427437
* @param mixed $relatedObject
@@ -433,7 +443,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array
433443
*/
434444
private function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context)
435445
{
436-
if ($propertyMetadata->isReadableLink()) {
446+
if ($propertyMetadata->isReadableLink() || !$this->hasIri($relatedObject)) {
437447
return $this->serializer->normalize($relatedObject, $format, $this->createRelationSerializationContext($resourceClass, $context));
438448
}
439449

@@ -444,4 +454,20 @@ private function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedO
444454

445455
return $iri;
446456
}
457+
458+
/**
459+
* Determines whether an item has an IRI.
460+
*
461+
* @param object $object
462+
*
463+
* @return bool
464+
*
465+
* @internal
466+
*/
467+
protected function hasIri($object): bool
468+
{
469+
$identifiers = $this->identifiersExtractor->getIdentifiersFromItem($object);
470+
471+
return (bool) array_filter($identifiers);
472+
}
447473
}

tests/Hal/Serializer/ItemNormalizerTest.php

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
namespace ApiPlatform\Core\Tests\Hal\Serializer;
1515

16+
use ApiPlatform\Core\Api\IdentifiersExtractorInterface;
1617
use ApiPlatform\Core\Api\IriConverterInterface;
1718
use ApiPlatform\Core\Api\ResourceClassResolverInterface;
1819
use ApiPlatform\Core\Exception\InvalidArgumentException;
@@ -34,18 +35,24 @@ class ItemNormalizerTest extends \PHPUnit_Framework_TestCase
3435
/**
3536
* @expectedException \ApiPlatform\Core\Exception\RuntimeException
3637
*/
37-
public function testDonTSupportDenormalization()
38+
public function testDontSupportDenormalization()
3839
{
3940
$propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class);
4041
$propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class);
4142
$iriConverterProphecy = $this->prophesize(IriConverterInterface::class);
4243
$resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class);
4344

45+
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
46+
4447
$normalizer = new ItemNormalizer(
4548
$propertyNameCollectionFactoryProphecy->reveal(),
4649
$propertyMetadataFactoryProphecy->reveal(),
4750
$iriConverterProphecy->reveal(),
48-
$resourceClassResolverProphecy->reveal()
51+
$resourceClassResolverProphecy->reveal(),
52+
null,
53+
null,
54+
null,
55+
$identifiersExtractorProphecy->reveal()
4956
);
5057

5158
$this->assertFalse($normalizer->supportsDenormalization('foo', ItemNormalizer::FORMAT));
@@ -66,11 +73,17 @@ public function testSupportNormalization()
6673
$resourceClassResolverProphecy->getResourceClass($dummy)->willReturn(Dummy::class)->shouldBeCalled();
6774
$resourceClassResolverProphecy->getResourceClass($std)->willThrow(new InvalidArgumentException())->shouldBeCalled();
6875

76+
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
77+
6978
$normalizer = new ItemNormalizer(
7079
$propertyNameCollectionFactoryProphecy->reveal(),
7180
$propertyMetadataFactoryProphecy->reveal(),
7281
$iriConverterProphecy->reveal(),
73-
$resourceClassResolverProphecy->reveal()
82+
$resourceClassResolverProphecy->reveal(),
83+
null,
84+
null,
85+
null,
86+
$identifiersExtractorProphecy->reveal()
7487
);
7588

7689
$this->assertTrue($normalizer->supportsNormalization($dummy, 'jsonhal'));
@@ -98,6 +111,9 @@ public function testNormalize()
98111
$resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled();
99112
$resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class, true)->willReturn(Dummy::class)->shouldBeCalled();
100113

114+
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
115+
$identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']);
116+
101117
$serializerProphecy = $this->prophesize(SerializerInterface::class);
102118
$serializerProphecy->willImplement(NormalizerInterface::class);
103119
$serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled();
@@ -106,7 +122,11 @@ public function testNormalize()
106122
$propertyNameCollectionFactoryProphecy->reveal(),
107123
$propertyMetadataFactoryProphecy->reveal(),
108124
$iriConverterProphecy->reveal(),
109-
$resourceClassResolverProphecy->reveal()
125+
$resourceClassResolverProphecy->reveal(),
126+
null,
127+
null,
128+
null,
129+
$identifiersExtractorProphecy->reveal()
110130
);
111131
$normalizer->setSerializer($serializerProphecy->reveal());
112132

@@ -141,6 +161,9 @@ public function testNormalizeWithoutCache()
141161
$resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled();
142162
$resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class, true)->willReturn(Dummy::class)->shouldBeCalled();
143163

164+
$identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class);
165+
$identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']);
166+
144167
$serializerProphecy = $this->prophesize(SerializerInterface::class);
145168
$serializerProphecy->willImplement(NormalizerInterface::class);
146169
$serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled();
@@ -149,7 +172,11 @@ public function testNormalizeWithoutCache()
149172
$propertyNameCollectionFactoryProphecy->reveal(),
150173
$propertyMetadataFactoryProphecy->reveal(),
151174
$iriConverterProphecy->reveal(),
152-
$resourceClassResolverProphecy->reveal()
175+
$resourceClassResolverProphecy->reveal(),
176+
null,
177+
null,
178+
null,
179+
$identifiersExtractorProphecy->reveal()
153180
);
154181
$normalizer->setSerializer($serializerProphecy->reveal());
155182

0 commit comments

Comments
 (0)