From 4c6d1441695786b77e47ad308618e969062db0c2 Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Fri, 24 Feb 2017 20:27:31 +0800 Subject: [PATCH] Allow blank nodes in JSON-LD --- .../Symfony/Bundle/Resources/config/api.xml | 1 + .../Symfony/Bundle/Resources/config/hal.xml | 1 + .../Bundle/Resources/config/jsonld.xml | 1 + src/Bridge/Symfony/Routing/IriConverter.php | 2 +- src/JsonLd/Serializer/ItemNormalizer.php | 65 ++++++++++++++++-- .../Util/BlankNodeIdentifiersGenerator.php | 51 ++++++++++++++ src/Serializer/AbstractItemNormalizer.php | 36 ++++++++-- tests/Hal/Serializer/ItemNormalizerTest.php | 37 +++++++++-- .../Serializer/ItemNormalizerTest.php | 52 +++++++-------- .../Serializer/AbstractItemNormalizerTest.php | 66 +++++++++++++++++++ tests/Serializer/ItemNormalizerTest.php | 42 ++++++++++-- 11 files changed, 305 insertions(+), 49 deletions(-) create mode 100644 src/JsonLd/Util/BlankNodeIdentifiersGenerator.php rename tests/{Hydra => JsonLd}/Serializer/ItemNormalizerTest.php (76%) diff --git a/src/Bridge/Symfony/Bundle/Resources/config/api.xml b/src/Bridge/Symfony/Bundle/Resources/config/api.xml index 2cd54de37bb..a50cdca7897 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/api.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/api.xml @@ -79,6 +79,7 @@ + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml index 9e6a3f5d358..340c1701340 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/hal.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/hal.xml @@ -34,6 +34,7 @@ + diff --git a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml index 924d1b5760d..4c8a398777a 100644 --- a/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml +++ b/src/Bridge/Symfony/Bundle/Resources/config/jsonld.xml @@ -25,6 +25,7 @@ + diff --git a/src/Bridge/Symfony/Routing/IriConverter.php b/src/Bridge/Symfony/Routing/IriConverter.php index fd8aa5a40db..a60377e7ea5 100644 --- a/src/Bridge/Symfony/Routing/IriConverter.php +++ b/src/Bridge/Symfony/Routing/IriConverter.php @@ -57,7 +57,7 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->propertyAccessor = $propertyAccessor ?? PropertyAccess::createPropertyAccessor(); if (null === $identifiersExtractor) { - @trigger_error('Not injecting ItemIdentifiersExtractor is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3'); + @trigger_error('Not injecting IdentifiersExtractorInterface is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3'); $this->identifiersExtractor = new IdentifiersExtractor($this->propertyNameCollectionFactory, $this->propertyMetadataFactory, $this->propertyAccessor); } else { $this->identifiersExtractor = $identifiersExtractor; diff --git a/src/JsonLd/Serializer/ItemNormalizer.php b/src/JsonLd/Serializer/ItemNormalizer.php index c096ae27faa..0a13e002701 100644 --- a/src/JsonLd/Serializer/ItemNormalizer.php +++ b/src/JsonLd/Serializer/ItemNormalizer.php @@ -13,10 +13,12 @@ namespace ApiPlatform\Core\JsonLd\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; use ApiPlatform\Core\JsonLd\ContextBuilderInterface; +use ApiPlatform\Core\JsonLd\Util\BlankNodeIdentifiersGenerator; use ApiPlatform\Core\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Core\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface; @@ -40,13 +42,16 @@ final class ItemNormalizer extends AbstractItemNormalizer private $resourceMetadataFactory; private $contextBuilder; + private $blankNodeIdentifiersGenerator; - public function __construct(ResourceMetadataFactoryInterface $resourceMetadataFactory, PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, ContextBuilderInterface $contextBuilder, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null) + 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) { - parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory); + parent::__construct($propertyNameCollectionFactory, $propertyMetadataFactory, $iriConverter, $resourceClassResolver, $propertyAccessor, $nameConverter, $classMetadataFactory, $identifiersExtractor); $this->resourceMetadataFactory = $resourceMetadataFactory; $this->contextBuilder = $contextBuilder; + + $this->blankNodeIdentifiersGenerator = new BlankNodeIdentifiersGenerator(); } /** @@ -68,14 +73,20 @@ public function normalize($object, $format = null, array $context = []) // Use resolved resource class instead of given resource class to support multiple inheritance child types $context['resource_class'] = $resourceClass; - $context['iri'] = $this->iriConverter->getIriFromItem($object); + + $context = $this->addJsonLdDocumentContext($object, $context); + + $jsonLdIdentifier = $this->getJsonLdNodeIdentifier($object, $context); + if ($this->hasIri($object)) { + $context['iri'] = $jsonLdIdentifier; + } $rawData = parent::normalize($object, $format, $context); if (!is_array($rawData)) { return $rawData; } - $data['@id'] = $context['iri']; + $data['@id'] = $jsonLdIdentifier; $data['@type'] = $resourceMetadata->getIri() ?: $resourceMetadata->getShortName(); return $data + $rawData; @@ -96,6 +107,12 @@ public function supportsDenormalization($data, $type, $format = null) */ public function denormalize($data, $class, $format = null, array $context = []) { + // Blank node identifiers cannot be used in denormalization + // Denormalize into new object + if (isset($data['@id']) && $this->isBlankNodeIdentifier($data['@id'])) { + unset($data['@id']); + } + // Avoid issues with proxies if we populated the object if (isset($data['@id']) && !isset($context['object_to_populate'])) { if (isset($context['api_allow_update']) && true !== $context['api_allow_update']) { @@ -107,4 +124,44 @@ public function denormalize($data, $class, $format = null, array $context = []) return parent::denormalize($data, $class, $format, $context); } + + /** + * Adds information related to the JSON-LD document to the serializer context. + * + * @param object $object + * @param array $context + * + * @return array + */ + private function addJsonLdDocumentContext($object, array $context) + { + $context['jsonld_document_root'] ?? $context['jsonld_document_root'] = spl_object_hash($object); + + return $context; + } + + /** + * Gets the identifier for a JSON-LD node. + * + * @param object $object + * @param array $context + * + * @return string + */ + private function getJsonLdNodeIdentifier($object, array $context): string + { + return $this->hasIri($object) ? $this->iriConverter->getIriFromItem($object) : $this->blankNodeIdentifiersGenerator->getBlankNodeIdentifier($object, $context['jsonld_document_root']); + } + + /** + * Determines whether an IRI is a JSON-LD blank node identifier. + * + * @param string $iri + * + * @return bool + */ + private function isBlankNodeIdentifier(string $iri): bool + { + return '_:' === substr($iri, 0, 2); + } } diff --git a/src/JsonLd/Util/BlankNodeIdentifiersGenerator.php b/src/JsonLd/Util/BlankNodeIdentifiersGenerator.php new file mode 100644 index 00000000000..f3517c33dc4 --- /dev/null +++ b/src/JsonLd/Util/BlankNodeIdentifiersGenerator.php @@ -0,0 +1,51 @@ + + * + * 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\Core\JsonLd\Util; + +/** + * Generates blank node identifiers scoped to each JSON-LD document. + * + * @author Teoh Han Hui + * + * @internal + */ +final class BlankNodeIdentifiersGenerator +{ + const IDENTIFIER_PREFIX = '_:b'; + + private $blankNodeCounts = []; + private $identifiers = []; + + /** + * Gets a blank node identifier for an object, scoped to a JSON-LD document. + * + * @param object $object + * @param string $documentRootHash + * + * @return string + */ + public function getBlankNodeIdentifier($object, string $documentRootHash): string + { + $objectHash = spl_object_hash($object); + + if (!isset($this->identifiers[$documentRootHash][$objectHash])) { + $this->blankNodeCounts[$documentRootHash] ?? $this->blankNodeCounts[$documentRootHash] = 0; + $this->identifiers[$documentRootHash] ?? $this->identifiers[$documentRootHash] = []; + + $this->identifiers[$documentRootHash][$objectHash] = sprintf('%s%d', self::IDENTIFIER_PREFIX, $this->blankNodeCounts[$documentRootHash]++); + } + + return $this->identifiers[$documentRootHash][$objectHash]; + } +} diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index 1d46fe5b103..a48f169955c 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -13,6 +13,8 @@ namespace ApiPlatform\Core\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractor; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\OperationType; use ApiPlatform\Core\Api\ResourceClassResolverInterface; @@ -43,8 +45,9 @@ abstract class AbstractItemNormalizer extends AbstractObjectNormalizer protected $iriConverter; protected $resourceClassResolver; protected $propertyAccessor; + protected $identifiersExtractor; - public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null) + public function __construct(PropertyNameCollectionFactoryInterface $propertyNameCollectionFactory, PropertyMetadataFactoryInterface $propertyMetadataFactory, IriConverterInterface $iriConverter, ResourceClassResolverInterface $resourceClassResolver, PropertyAccessorInterface $propertyAccessor = null, NameConverterInterface $nameConverter = null, ClassMetadataFactoryInterface $classMetadataFactory = null, IdentifiersExtractorInterface $identifiersExtractor = null) { parent::__construct($classMetadataFactory, $nameConverter); @@ -54,8 +57,15 @@ public function __construct(PropertyNameCollectionFactoryInterface $propertyName $this->resourceClassResolver = $resourceClassResolver; $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor(); + if (null === $identifiersExtractor) { + @trigger_error('Not injecting IdentifiersExtractorInterface is deprecated since API Platform 2.1 and will not be possible anymore in API Platform 3'); + $this->identifiersExtractor = new IdentifiersExtractor($this->propertyNameCollectionFactory, $this->propertyMetadataFactory, $this->propertyAccessor); + } else { + $this->identifiersExtractor = $identifiersExtractor; + } + $this->setCircularReferenceHandler(function ($object) { - return $this->iriConverter->getIriFromItem($object); + return $this->hasIri($object) ? $this->iriConverter->getIriFromItem($object) : spl_object_hash($object); }); } @@ -86,7 +96,7 @@ public function normalize($object, $format = null, array $context = []) $context = $this->initContext($resourceClass, $context); $context['api_normalize'] = true; - if (isset($context['resources'])) { + if (isset($context['resources']) && $this->hasIri($object)) { $resource = $context['iri'] ?? $this->iriConverter->getIriFromItem($object); $context['resources'][$resource] = $resource; } @@ -421,7 +431,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array } /** - * Normalizes a relation as an URI if is a Link or as a JSON-LD object. + * Normalizes a relation. * * @param PropertyMetadata $propertyMetadata * @param mixed $relatedObject @@ -433,7 +443,7 @@ protected function getAttributeValue($object, $attribute, $format = null, array */ private function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedObject, string $resourceClass, string $format = null, array $context) { - if ($propertyMetadata->isReadableLink()) { + if ($propertyMetadata->isReadableLink() || !$this->hasIri($relatedObject)) { return $this->serializer->normalize($relatedObject, $format, $this->createRelationSerializationContext($resourceClass, $context)); } @@ -444,4 +454,20 @@ private function normalizeRelation(PropertyMetadata $propertyMetadata, $relatedO return $iri; } + + /** + * Determines whether an item has an IRI. + * + * @param object $object + * + * @return bool + * + * @internal + */ + protected function hasIri($object): bool + { + $identifiers = $this->identifiersExtractor->getIdentifiersFromItem($object); + + return (bool) array_filter($identifiers); + } } diff --git a/tests/Hal/Serializer/ItemNormalizerTest.php b/tests/Hal/Serializer/ItemNormalizerTest.php index bb3cea8b279..acdd5445ecd 100644 --- a/tests/Hal/Serializer/ItemNormalizerTest.php +++ b/tests/Hal/Serializer/ItemNormalizerTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Hal\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -34,18 +35,24 @@ class ItemNormalizerTest extends \PHPUnit_Framework_TestCase /** * @expectedException \ApiPlatform\Core\Exception\RuntimeException */ - public function testDonTSupportDenormalization() + public function testDontSupportDenormalization() { $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $normalizer = new ItemNormalizer( $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $this->assertFalse($normalizer->supportsDenormalization('foo', ItemNormalizer::FORMAT)); @@ -66,11 +73,17 @@ public function testSupportNormalization() $resourceClassResolverProphecy->getResourceClass($dummy)->willReturn(Dummy::class)->shouldBeCalled(); $resourceClassResolverProphecy->getResourceClass($std)->willThrow(new InvalidArgumentException())->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $normalizer = new ItemNormalizer( $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $this->assertTrue($normalizer->supportsNormalization($dummy, 'jsonhal')); @@ -98,6 +111,9 @@ public function testNormalize() $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class, true)->willReturn(Dummy::class)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled(); @@ -106,7 +122,11 @@ public function testNormalize() $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -141,6 +161,9 @@ public function testNormalizeWithoutCache() $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class, true)->willReturn(Dummy::class)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled(); @@ -149,7 +172,11 @@ public function testNormalizeWithoutCache() $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); diff --git a/tests/Hydra/Serializer/ItemNormalizerTest.php b/tests/JsonLd/Serializer/ItemNormalizerTest.php similarity index 76% rename from tests/Hydra/Serializer/ItemNormalizerTest.php rename to tests/JsonLd/Serializer/ItemNormalizerTest.php index 14fa9e73162..0a235b3098f 100644 --- a/tests/Hydra/Serializer/ItemNormalizerTest.php +++ b/tests/JsonLd/Serializer/ItemNormalizerTest.php @@ -11,8 +11,9 @@ declare(strict_types=1); -namespace ApiPlatform\Core\Tests\Hydra\Serializer; +namespace ApiPlatform\Core\Tests\JsonLd\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -44,9 +45,11 @@ public function testDontSupportDenormalization() $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); $resourceClassResolverProphecy->getResourceClass(['dummy'], 'Dummy')->willReturn(Dummy::class); $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn(new PropertyNameCollection(['name' => 'name'])); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(new PropertyMetadata())->shouldBeCalled(1); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn(new PropertyMetadata()); - $normalizer = new ItemNormalizer($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $contextBuilderProphecy->reveal()); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + + $normalizer = new ItemNormalizer($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $contextBuilderProphecy->reveal(), null, null, null, $identifiersExtractorProphecy->reveal()); $this->assertFalse($normalizer->supportsDenormalization('foo', ItemNormalizer::FORMAT)); $normalizer->denormalize(['foo'], Dummy::class, 'jsonld', ['jsonld_has_context' => true, 'jsonld_sub_level' => true, 'resource_class' => Dummy::class]); @@ -65,17 +68,12 @@ public function testSupportNormalization() $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->getResourceClass($dummy)->willReturn(Dummy::class)->shouldBeCalled(); - $resourceClassResolverProphecy->getResourceClass($std)->willThrow(new InvalidArgumentException())->shouldBeCalled(); - - $normalizer = new ItemNormalizer( - $resourceMetadataFactoryProphecy->reveal(), - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $contextBuilderProphecy->reveal() - ); + $resourceClassResolverProphecy->getResourceClass($dummy)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($std)->willThrow(new InvalidArgumentException()); + + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + + $normalizer = new ItemNormalizer($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $contextBuilderProphecy->reveal(), null, null, null, $identifiersExtractorProphecy->reveal()); $this->assertTrue($normalizer->supportsNormalization($dummy, 'jsonld')); $this->assertFalse($normalizer->supportsNormalization($dummy, 'xml')); @@ -91,33 +89,29 @@ public function testNormalize() $resourceMetadataFactoryProphecy->create(Dummy::class)->willReturn(new ResourceMetadata('Dummy')); $propertyNameCollection = new PropertyNameCollection(['name']); $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); - $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection)->shouldBeCalled(); + $propertyNameCollectionFactoryProphecy->create(Dummy::class, [])->willReturn($propertyNameCollection); - $propertyMetadataFactory = new PropertyMetadata(null, null, true); + $propertyMetadataProphecy = new PropertyMetadata(null, null, true); $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); - $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadataFactory)->shouldBeCalled(); + $propertyMetadataFactoryProphecy->create(Dummy::class, 'name', [])->willReturn($propertyMetadataProphecy); $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); - $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummies/1988')->shouldBeCalled(); + $iriConverterProphecy->getIriFromItem($dummy)->willReturn('/dummies/1988'); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); - $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); - $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class, true)->willReturn(Dummy::class)->shouldBeCalled(); + $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class); + $resourceClassResolverProphecy->getResourceClass($dummy, Dummy::class, true)->willReturn(Dummy::class); $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); - $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled(); + $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello'); $contextBuilderProphecy = $this->prophesize(ContextBuilderInterface::class); $contextBuilderProphecy->getResourceContextUri(Dummy::class)->willReturn('/contexts/Dummy'); - $normalizer = new ItemNormalizer( - $resourceMetadataFactoryProphecy->reveal(), - $propertyNameCollectionFactoryProphecy->reveal(), - $propertyMetadataFactoryProphecy->reveal(), - $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal(), - $contextBuilderProphecy->reveal() - ); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + + $normalizer = new ItemNormalizer($resourceMetadataFactoryProphecy->reveal(), $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $contextBuilderProphecy->reveal(), null, null, null, $identifiersExtractorProphecy->reveal()); $normalizer->setSerializer($serializerProphecy->reveal()); $expected = ['@context' => '/contexts/Dummy', diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index ab23083538e..b814c4d5c49 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -56,12 +57,17 @@ public function testSupportNormalizationAndSupportDenormalization() $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true)->shouldBeCalled(); $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $this->assertTrue($normalizer->supportsNormalization($dummy)); @@ -126,6 +132,10 @@ public function testNormalize() $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(RelatedDummy::class)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + $identifiersExtractorProphecy->getIdentifiersFromItem($relatedDummy)->willReturn(['id']); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo')->shouldBeCalled(); @@ -137,6 +147,9 @@ public function testNormalize() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->setIgnoredAttributes(['alias']); @@ -193,6 +206,10 @@ public function testNormalizeReadableLinks() $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(RelatedDummy::class)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + $identifiersExtractorProphecy->getIdentifiersFromItem($relatedDummy)->willReturn(['id']); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize($relatedDummy, null, Argument::type('array'))->willReturn(['foo' => 'hello'])->shouldBeCalled(); @@ -205,6 +222,9 @@ public function testNormalizeReadableLinks() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -266,6 +286,8 @@ public function testDenormalize() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); @@ -275,6 +297,9 @@ public function testDenormalize() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -332,6 +357,8 @@ public function testDenormalizeWritableLinks() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); $serializerProphecy->denormalize( @@ -347,6 +374,9 @@ public function testDenormalizeWritableLinks() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -386,6 +416,8 @@ public function testBadRelationType() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -395,6 +427,9 @@ public function testBadRelationType() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -430,6 +465,8 @@ public function testInnerDocumentNotAllowed() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->isResourceClass(RelatedDummy::class)->willReturn(true)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -439,6 +476,9 @@ public function testInnerDocumentNotAllowed() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -465,6 +505,8 @@ public function testBadType() $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -474,6 +516,9 @@ public function testBadType() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -497,6 +542,8 @@ public function testJsonAllowIntAsFloat() $propertyAccessorProphecy->setValue(Argument::type(Dummy::class), 'foo', 42)->shouldBeCalled(); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -506,6 +553,9 @@ public function testJsonAllowIntAsFloat() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -547,6 +597,8 @@ public function testDenormalizeBadKeyType() $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -556,6 +608,9 @@ public function testDenormalizeBadKeyType() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -582,6 +637,8 @@ public function testNullable() $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -591,6 +648,9 @@ public function testNullable() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -625,6 +685,9 @@ public function testChildInheritedProperty() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($dummy, DummyTableInheritance::class, true)->willReturn(DummyTableInheritance::class)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize('foo', null, Argument::type('array'))->willReturn('foo')->shouldBeCalled(); @@ -636,6 +699,9 @@ public function testChildInheritedProperty() $iriConverterProphecy->reveal(), $resourceClassResolverProphecy->reveal(), $propertyAccessorProphecy->reveal(), + null, + null, + $identifiersExtractorProphecy->reveal(), ]); $normalizer->setSerializer($serializerProphecy->reveal()); diff --git a/tests/Serializer/ItemNormalizerTest.php b/tests/Serializer/ItemNormalizerTest.php index d7b06de224d..4e01c192b2a 100644 --- a/tests/Serializer/ItemNormalizerTest.php +++ b/tests/Serializer/ItemNormalizerTest.php @@ -13,6 +13,7 @@ namespace ApiPlatform\Core\Tests\Serializer; +use ApiPlatform\Core\Api\IdentifiersExtractorInterface; use ApiPlatform\Core\Api\IriConverterInterface; use ApiPlatform\Core\Api\ResourceClassResolverInterface; use ApiPlatform\Core\Exception\InvalidArgumentException; @@ -48,11 +49,17 @@ public function testSupportNormalization() $resourceClassResolverProphecy->isResourceClass(Dummy::class)->willReturn(true)->shouldBeCalled(); $resourceClassResolverProphecy->isResourceClass(\stdClass::class)->willReturn(false)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $normalizer = new ItemNormalizer( $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $this->assertTrue($normalizer->supportsNormalization($dummy)); @@ -83,6 +90,9 @@ public function testNormalize() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); $resourceClassResolverProphecy->getResourceClass($dummy, null, true)->willReturn(Dummy::class)->shouldBeCalled(); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $identifiersExtractorProphecy->getIdentifiersFromItem($dummy)->willReturn(['id']); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(NormalizerInterface::class); $serializerProphecy->normalize('hello', null, Argument::type('array'))->willReturn('hello')->shouldBeCalled(); @@ -91,7 +101,11 @@ public function testNormalize() $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -114,6 +128,8 @@ public function testDenormalize() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -121,7 +137,11 @@ public function testDenormalize() $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -145,6 +165,8 @@ public function testDenormalizeWithIri() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -152,7 +174,11 @@ public function testDenormalizeWithIri() $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); @@ -175,6 +201,8 @@ public function testDenormalizeWithIdAndUpdateNotAllowed() $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + $identifiersExtractorProphecy = $this->prophesize(IdentifiersExtractorInterface::class); + $serializerProphecy = $this->prophesize(SerializerInterface::class); $serializerProphecy->willImplement(DenormalizerInterface::class); @@ -182,7 +210,11 @@ public function testDenormalizeWithIdAndUpdateNotAllowed() $propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), $iriConverterProphecy->reveal(), - $resourceClassResolverProphecy->reveal() + $resourceClassResolverProphecy->reveal(), + null, + null, + null, + $identifiersExtractorProphecy->reveal() ); $normalizer->setSerializer($serializerProphecy->reveal()); $normalizer->denormalize(['id' => '12', 'name' => 'hello'], Dummy::class, null, $context);