diff --git a/features/jsonld/iri_only.feature b/features/jsonld/iri_only.feature index d2ac3db1323..2ec4f864170 100644 --- a/features/jsonld/iri_only.feature +++ b/features/jsonld/iri_only.feature @@ -56,3 +56,37 @@ Feature: JSON-LD using iri_only parameter "hydra:totalItems": 3 } """ + + Scenario: Retrieve Resource with iriOnly collection Property + Given there are propertyCollectionIriOnly with relations + When I send a "GET" request to "/property_collection_iri_onlies" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "hydra:member": [ + { + "@id": "/property_collection_iri_onlies/1", + "@type": "PropertyCollectionIriOnly", + "propertyCollectionIriOnlyRelation": "/property_collection_iri_only_relations", + "iterableIri": "/property_collection_iri_only_relations" + }, + ], + } + """ + When I send a "GET" request to "/property_collection_iri_onlies/1" + Then the response status code should be 200 + And the response should be in JSON + And the header "Content-Type" should be equal to "application/ld+json; charset=utf-8" + And the JSON should be valid according to this schema: + """ + { + "@context": "/contexts/PropertyCollectionIriOnly", + "@id": "/property_collection_iri_onlies/1", + "@type": "PropertyCollectionIriOnly", + "propertyCollectionIriOnlyRelation": "/property_collection_iri_only_relations", + "iterableIri": "/property_collection_iri_only_relations" + } + """ diff --git a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php index eb7f939f47e..1f5101f16a5 100644 --- a/src/Doctrine/Orm/Extension/EagerLoadingExtension.php +++ b/src/Doctrine/Orm/Extension/EagerLoadingExtension.php @@ -155,8 +155,9 @@ private function joinRelations(QueryBuilder $queryBuilder, QueryNameGeneratorInt } $fetchEager = $propertyMetadata->getFetchEager(); + $iriOnly = $propertyMetadata->getIriOnly(); - if (false === $fetchEager) { + if (false === $fetchEager || true === $iriOnly) { continue; } diff --git a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php index cf1c294e19e..3310b92c225 100644 --- a/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php +++ b/src/JsonSchema/Metadata/Property/Factory/SchemaPropertyMetadataFactory.php @@ -49,7 +49,7 @@ public function create(string $resourceClass, string $property, array $options = $propertySchema = $propertyMetadata->getSchema() ?? []; - if (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable()) { + if (true === $propertyMetadata->getIriOnly() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) { $propertySchema['readOnly'] = true; } @@ -124,6 +124,11 @@ public function create(string $resourceClass, string $property, array $options = $propertySchema['owl:maxCardinality'] = 1; } + if ($isCollection && $propertyMetadata->getIriOnly()){ + $keyType = null; + $isCollection = false; + } + $propertyType = $this->getType(new Type($builtinType, $type->isNullable(), $className, $isCollection, $keyType, $valueType), $propertyMetadata->isReadableLink()); if (!\in_array($propertyType, $valueSchema, true)) { $valueSchema[] = $propertyType; diff --git a/src/Metadata/ApiProperty.php b/src/Metadata/ApiProperty.php index 8541dc210af..db87ba1ca1b 100644 --- a/src/Metadata/ApiProperty.php +++ b/src/Metadata/ApiProperty.php @@ -40,6 +40,7 @@ final class ApiProperty * @param string[] $types the RDF types of this property * @param string[] $iris * @param Type[] $builtinTypes + * @param bool[] $iriOnly Whether to return the subRessource collection IRI instead of an iterable of IRI. */ public function __construct( private ?string $description = null, @@ -69,7 +70,8 @@ public function __construct( private ?bool $initializable = null, private $iris = null, private ?bool $genId = null, - private array $extraProperties = [] + private array $extraProperties = [], + private ?bool $iriOnly = null, ) { if (\is_string($types)) { $this->types = (array) $types; @@ -420,4 +422,20 @@ public function withGenId(bool $genId): self return $metadata; } + + /** + * Whether to return the subRessource collection IRI instead of an iterable of IRI. + */ + public function getIriOnly() + { + return $this->iriOnly; + } + + public function withIriOnly(bool $iriOnly): self + { + $metadata = clone $this; + $metadata->iriOnly = $iriOnly; + + return $metadata; + } } diff --git a/src/Serializer/AbstractItemNormalizer.php b/src/Serializer/AbstractItemNormalizer.php index fd6533012ba..84ccbbb5f25 100644 --- a/src/Serializer/AbstractItemNormalizer.php +++ b/src/Serializer/AbstractItemNormalizer.php @@ -20,6 +20,7 @@ use ApiPlatform\Exception\ItemNotFoundException; use ApiPlatform\Metadata\ApiProperty; use ApiPlatform\Metadata\CollectionOperationInterface; +use ApiPlatform\Metadata\GetCollection; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; @@ -623,6 +624,13 @@ protected function getAttributeValue(object $object, string $attribute, string $ $childContext = $this->createChildContext($context, $attribute, $format); unset($childContext['iri'], $childContext['uri_variables'], $childContext['resource_class'], $childContext['operation']); + if (true === $propertyMetadata->getIriOnly()) { + $operation = $this->resourceMetadataCollectionFactory->create($resourceClass)->getOperation(null, true); + if ($operation instanceof GetCollection) { + return $this->iriConverter->getIriFromResource($resourceClass, UrlGeneratorInterface::ABS_PATH, $operation, $childContext); + } + } + return $this->normalizeCollectionOfRelations($propertyMetadata, $attributeValue, $resourceClass, $format, $childContext); } diff --git a/tests/Behat/DoctrineContext.php b/tests/Behat/DoctrineContext.php index a271b8d895b..b77b8006fdf 100644 --- a/tests/Behat/DoctrineContext.php +++ b/tests/Behat/DoctrineContext.php @@ -76,6 +76,8 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Document\Pet as PetDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\Product as ProductDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\Program as ProgramDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\PropertyCollectionIriOnly as PropertyCollectionIriOnlyDocument; +use ApiPlatform\Tests\Fixtures\TestBundle\Document\PropertyCollectionIriOnlyRelation as PropertyCollectionIriOnlyRelationDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\Question as QuestionDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedDummy as RelatedDummyDocument; use ApiPlatform\Tests\Fixtures\TestBundle\Document\RelatedOwnedDummy as RelatedOwnedDummyDocument; @@ -159,6 +161,8 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Pet; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Product; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Program; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnly; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnlyRelation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Question; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RamseyUuidDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; @@ -1945,6 +1949,22 @@ public function thereAreIriOnlyDummies(int $nb): void $this->manager->flush(); } + /** + * @Given there are propertyCollectionIriOnly with relations + */ + public function thereAreIriOnlyCollections(): void + { + $propertyCollectionIriOnlyRelation = $this->isOrm() ? new PropertyCollectionIriOnlyRelation() : new PropertyCollectionIriOnlyRelationDocument(); + $propertyCollectionIriOnlyRelation->name = 'relation'; + + $propertyCollectionIriOnly = $this->isOrm() ? new PropertyCollectionIriOnly() : new PropertyCollectionIriOnlyDocument(); + $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation); + + $this->manager->persist($propertyCollectionIriOnly); + $this->manager->persist($propertyCollectionIriOnlyRelation); + $this->manager->flush(); + } + /** * @Given there are :nb absoluteUrlDummy objects with a related absoluteUrlRelationDummy */ diff --git a/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php b/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php index c9b1da162cd..5ad3b2b349f 100644 --- a/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php +++ b/tests/Doctrine/Orm/Extension/EagerLoadingExtensionTest.php @@ -28,6 +28,8 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\ConcreteDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\Dummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\EmbeddableDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnly; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnlyRelation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\UnknownDummy; use Doctrine\ORM\EntityManager; @@ -884,4 +886,39 @@ public function testApplyToCollectionWithAReadableButNotFetchEagerProperty(): vo $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30); $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), Dummy::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'foo'])); } + + public function testAvoidFetchCollectionOnIriOnlyProperty(): void + { + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $relationPropertyMetadata = new ApiProperty(); + $relationPropertyMetadata = $relationPropertyMetadata->withFetchEager(true); + $relationPropertyMetadata = $relationPropertyMetadata->withReadableLink(true); + $relationPropertyMetadata = $relationPropertyMetadata->withReadable(true); + $relationPropertyMetadata = $relationPropertyMetadata->withIriOnly(true); + + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ['serializer_groups' => ['read'], 'normalization_groups' => 'read'])->willReturn($relationPropertyMetadata)->shouldBeCalled(); + + $queryBuilderProphecy = $this->prophesize(QueryBuilder::class); + + $classMetadataProphecy = $this->prophesize(ClassMetadata::class); + $classMetadataProphecy->associationMappings = [ + 'propertyCollectionIriOnlyRelation' => ['fetch' => ClassMetadataInfo::FETCH_EAGER, 'joinColumns' => [['nullable' => true]], 'targetEntity' => PropertyCollectionIriOnlyRelation::class], + ]; + + $emProphecy = $this->prophesize(EntityManager::class); + $emProphecy->getClassMetadata(PropertyCollectionIriOnly::class)->shouldBeCalled()->willReturn($classMetadataProphecy->reveal()); + $emProphecy->getClassMetadata(PropertyCollectionIriOnlyRelation::class)->shouldNotBecalled(); + + $queryBuilderProphecy->getRootAliases()->willReturn(['o']); + $queryBuilderProphecy->getEntityManager()->willReturn($emProphecy); + + $queryBuilderProphecy->leftJoin('o.propertyCollectionIriOnlyRelation', 'propertyCollectionIriOnlyRelation_a1')->shouldNotBeCalled(); + $queryBuilderProphecy->addSelect('propertyCollectionIriOnlyRelation_a1')->shouldNotBeCalled(); + + $queryBuilder = $queryBuilderProphecy->reveal(); + $eagerExtensionTest = new EagerLoadingExtension($propertyNameCollectionFactoryProphecy->reveal(), $propertyMetadataFactoryProphecy->reveal(), 30); + $eagerExtensionTest->applyToCollection($queryBuilder, new QueryNameGenerator(), PropertyCollectionIriOnly::class, new GetCollection(normalizationContext: [AbstractNormalizer::GROUPS => 'read'])); + } } diff --git a/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnly.php b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnly.php new file mode 100644 index 00000000000..efdf11b5580 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnly.php @@ -0,0 +1,90 @@ + ['read']]), GetCollection(normalizationContext: ['groups' => ['read']]), Post] +#[ODM\Document] +class PropertyCollectionIriOnly +{ + #[ODM\Id(strategy: 'INCREMENT', type: 'int')] + private ?int $id = null; + + #[ODM\ReferenceMany(targetDocument: PropertyCollectionIriOnlyRelation::class)] + #[ApiProperty(iriOnly: true)] + #[Groups('read')] + private Collection $propertyCollectionIriOnlyRelation; + + /** + * @var iterable $iterableIri + */ + #[ApiProperty(iriOnly: true)] + #[Groups('read')] + private array $iterableIri = []; + + public function __construct() + { + $this->propertyCollectionIriOnlyRelation = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getPropertyCollectionIriOnlyRelation(): Collection + { + return $this->propertyCollectionIriOnlyRelation; + } + + public function addPropertyCollectionIriOnlyRelation(PropertyCollectionIriOnlyRelation $propertyCollectionIriOnlyRelation): self + { + if (!$this->propertyCollectionIriOnlyRelation->contains($propertyCollectionIriOnlyRelation)) { + $this->propertyCollectionIriOnlyRelation->add($propertyCollectionIriOnlyRelation); + $propertyCollectionIriOnlyRelation->setPropertyCollectionIriOnly($this); + } + + return $this; + } + + public function removePropertyCollectionIriOnlyRelation(PropertyCollectionIriOnlyRelation $propertyCollectionIriOnlyRelation): self + { + if ($this->propertyCollectionIriOnlyRelation->removeElement($propertyCollectionIriOnlyRelation)) { + // set the owning side to null (unless already changed) + if ($propertyCollectionIriOnlyRelation->getPropertyCollectionIriOnly() === $this) { + $propertyCollectionIriOnlyRelation->setPropertyCollectionIriOnly(null); + } + } + + return $this; + } + + /** + * @return array + */ + public function getIterableIri(): array + { + $propertyCollectionIriOnlyRelation = new PropertyCollectionIriOnlyRelation(); + $propertyCollectionIriOnlyRelation->name = 'Michel'; + + $this->iterableIri[] = $propertyCollectionIriOnlyRelation; + + return $this->iterableIri; + } +} diff --git a/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php new file mode 100644 index 00000000000..4fb367444c3 --- /dev/null +++ b/tests/Fixtures/TestBundle/Document/PropertyCollectionIriOnlyRelation.php @@ -0,0 +1,49 @@ +id ?? 9999; + } + + /** + * @return PropertyCollectionIriOnly|null + */ + public function getPropertyCollectionIriOnly(): ?PropertyCollectionIriOnly + { + return $this->propertyCollectionIriOnly; + } + + /** + * @param PropertyCollectionIriOnly|null $propertyCollectionIriOnly + */ + public function setPropertyCollectionIriOnly(?PropertyCollectionIriOnly $propertyCollectionIriOnly): void + { + $this->propertyCollectionIriOnly = $propertyCollectionIriOnly; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php new file mode 100644 index 00000000000..a9364f6238a --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnly.php @@ -0,0 +1,93 @@ + ['read']]), GetCollection(normalizationContext: ['groups' => ['read']]), Post] +#[ORM\Entity] +class PropertyCollectionIriOnly +{ + + #[ORM\Id] + #[ORM\Column(type: 'integer')] + #[ORM\GeneratedValue] + private ?int $id = null; + + #[ORM\OneToMany(mappedBy: 'propertyCollectionIriOnly', targetEntity: PropertyCollectionIriOnlyRelation::class)] + #[ApiProperty(iriOnly: true)] + #[Groups('read')] + private Collection $propertyCollectionIriOnlyRelation; + + /** + * @var iterable $iterableIri + */ + #[ApiProperty(iriOnly: true)] + #[Groups('read')] + private array $iterableIri = []; + + public function __construct() + { + $this->propertyCollectionIriOnlyRelation = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + /** + * @return Collection + */ + public function getPropertyCollectionIriOnlyRelation(): Collection + { + return $this->propertyCollectionIriOnlyRelation; + } + + public function addPropertyCollectionIriOnlyRelation(PropertyCollectionIriOnlyRelation $propertyCollectionIriOnlyRelation): self + { + if (!$this->propertyCollectionIriOnlyRelation->contains($propertyCollectionIriOnlyRelation)) { + $this->propertyCollectionIriOnlyRelation->add($propertyCollectionIriOnlyRelation); + $propertyCollectionIriOnlyRelation->setPropertyCollectionIriOnly($this); + } + + return $this; + } + + public function removePropertyCollectionIriOnlyRelation(PropertyCollectionIriOnlyRelation $propertyCollectionIriOnlyRelation): self + { + if ($this->propertyCollectionIriOnlyRelation->removeElement($propertyCollectionIriOnlyRelation)) { + // set the owning side to null (unless already changed) + if ($propertyCollectionIriOnlyRelation->getPropertyCollectionIriOnly() === $this) { + $propertyCollectionIriOnlyRelation->setPropertyCollectionIriOnly(null); + } + } + + return $this; + } + + /** + * @return array + */ + public function getIterableIri(): array + { + $propertyCollectionIriOnlyRelation = new PropertyCollectionIriOnlyRelation(); + $propertyCollectionIriOnlyRelation->name = 'Michel'; + + $this->iterableIri[] = $propertyCollectionIriOnlyRelation; + + return $this->iterableIri; + } +} diff --git a/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php new file mode 100644 index 00000000000..a84811828d6 --- /dev/null +++ b/tests/Fixtures/TestBundle/Entity/PropertyCollectionIriOnlyRelation.php @@ -0,0 +1,53 @@ +id ?? 9999; + } + + /** + * @return PropertyCollectionIriOnly|null + */ + public function getPropertyCollectionIriOnly(): ?PropertyCollectionIriOnly + { + return $this->propertyCollectionIriOnly; + } + + /** + * @param PropertyCollectionIriOnly|null $propertyCollectionIriOnly + */ + public function setPropertyCollectionIriOnly(?PropertyCollectionIriOnly $propertyCollectionIriOnly): void + { + $this->propertyCollectionIriOnly = $propertyCollectionIriOnly; + } +} diff --git a/tests/Serializer/AbstractItemNormalizerTest.php b/tests/Serializer/AbstractItemNormalizerTest.php index 1b04021cce4..91301906ee1 100644 --- a/tests/Serializer/AbstractItemNormalizerTest.php +++ b/tests/Serializer/AbstractItemNormalizerTest.php @@ -15,10 +15,16 @@ use ApiPlatform\Api\IriConverterInterface; use ApiPlatform\Api\ResourceClassResolverInterface; +use ApiPlatform\Api\UrlGeneratorInterface; use ApiPlatform\Metadata\ApiProperty; +use ApiPlatform\Metadata\ApiResource; +use ApiPlatform\Metadata\GetCollection; +use ApiPlatform\Metadata\Operations; use ApiPlatform\Metadata\Property\Factory\PropertyMetadataFactoryInterface; use ApiPlatform\Metadata\Property\Factory\PropertyNameCollectionFactoryInterface; use ApiPlatform\Metadata\Property\PropertyNameCollection; +use ApiPlatform\Metadata\Resource\Factory\ResourceMetadataCollectionFactoryInterface; +use ApiPlatform\Metadata\Resource\ResourceMetadataCollection; use ApiPlatform\Serializer\AbstractItemNormalizer; use ApiPlatform\Symfony\Security\ResourceAccessCheckerInterface; use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\Issue5584\DtoWithNullValue; @@ -27,6 +33,8 @@ use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceChild; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\DummyTableInheritanceRelated; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\NonCloneableDummy; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnly; +use ApiPlatform\Tests\Fixtures\TestBundle\Entity\PropertyCollectionIriOnlyRelation; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\RelatedDummy; use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SecuredDummy; use Doctrine\Common\Collections\ArrayCollection; @@ -38,6 +46,9 @@ use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Serializer\Exception\NotNormalizableValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException; +use Symfony\Component\Serializer\Mapping\AttributeMetadataInterface; +use Symfony\Component\Serializer\Mapping\ClassMetadataInterface; +use Symfony\Component\Serializer\Mapping\Factory\ClassMetadataFactoryInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\SerializerInterface; @@ -215,6 +226,77 @@ public function testNormalizeWithSecuredProperty(): void ])); } + public function testNormalizeCollectionPropertyAsStringOnIriOnly(): void + { + $propertyCollectionIriOnlyRelation = new PropertyCollectionIriOnlyRelation(); + $propertyCollectionIriOnlyRelation->name = 'My Relation'; + + $propertyCollectionIriOnly = new PropertyCollectionIriOnly(); + $propertyCollectionIriOnly->addPropertyCollectionIriOnlyRelation($propertyCollectionIriOnlyRelation); + + $resourceRelationMetadataCollection = new ResourceMetadataCollection(PropertyCollectionIriOnlyRelation::class, [ + (new ApiResource())->withOperations(new Operations([new GetCollection('/propertyCollectionIriOnlyRelations')])) + ]); + + $resourceMetadataCollectionFactoryProphecy = $this->prophesize(ResourceMetadataCollectionFactoryInterface::class); + $resourceMetadataCollectionFactoryProphecy->create(PropertyCollectionIriOnlyRelation::class)->willReturn($resourceRelationMetadataCollection); + + $propertyNameCollectionFactoryProphecy = $this->prophesize(PropertyNameCollectionFactoryInterface::class); + $propertyNameCollectionFactoryProphecy->create(PropertyCollectionIriOnly::class, ["normalization_groups" => null, "denormalization_groups" => null, "operation_name" => null])->willReturn( + new PropertyNameCollection(['propertyCollectionIriOnlyRelation']) + ); + + $propertyMetadataFactoryProphecy = $this->prophesize(PropertyMetadataFactoryInterface::class); + $propertyMetadataFactoryProphecy->create(PropertyCollectionIriOnly::class, 'propertyCollectionIriOnlyRelation', ["normalization_groups" => null, "denormalization_groups" => null, "operation_name" => null])->willReturn( + (new ApiProperty())->withReadable(true)->withIriOnly(true)->withBuiltinTypes([ + new Type('iterable', false, null, true, new Type('int', false, null, false), new Type('object', false, PropertyCollectionIriOnlyRelation::class, false)) + ]) + ); + + $iriConverterProphecy = $this->prophesize(IriConverterInterface::class); + $iriConverterProphecy->getIriFromResource(PropertyCollectionIriOnlyRelation::class, UrlGeneratorInterface::ABS_PATH, new GetCollection('/propertyCollectionIriOnlyRelations'), Argument::any())->willReturn('/propertyCollectionIriOnlyRelations'); + $iriConverterProphecy->getIriFromResource($propertyCollectionIriOnly, UrlGeneratorInterface::ABS_URL, null, Argument::any())->willReturn('/propertyCollectionIriOnly'); + + $propertyAccessorProphecy = $this->prophesize(PropertyAccessorInterface::class); + $propertyAccessorProphecy->getValue($propertyCollectionIriOnly, 'propertyCollectionIriOnlyRelation')->willReturn([$propertyCollectionIriOnlyRelation]); + + $resourceClassResolverProphecy = $this->prophesize(ResourceClassResolverInterface::class); + + $resourceClassResolverProphecy->isResourceClass(PropertyCollectionIriOnly::class)->willReturn(true); + $resourceClassResolverProphecy->getResourceClass($propertyCollectionIriOnly, null)->willReturn(PropertyCollectionIriOnly::class); + $resourceClassResolverProphecy->getResourceClass(null, PropertyCollectionIriOnly::class)->willReturn(PropertyCollectionIriOnly::class); + + $resourceClassResolverProphecy->isResourceClass(PropertyCollectionIriOnlyRelation::class)->willReturn(true); + $resourceClassResolverProphecy->getResourceClass([$propertyCollectionIriOnlyRelation], null)->willReturn(PropertyCollectionIriOnlyRelation::class); + $resourceClassResolverProphecy->getResourceClass(null, PropertyCollectionIriOnlyRelation::class)->willReturn(PropertyCollectionIriOnlyRelation::class); + $resourceClassResolverProphecy->getResourceClass([$propertyCollectionIriOnlyRelation], PropertyCollectionIriOnlyRelation::class)->willReturn(PropertyCollectionIriOnlyRelation::class); + + $normalizer = $this->getMockForAbstractClass(AbstractItemNormalizer::class, [ + $propertyNameCollectionFactoryProphecy->reveal(), + $propertyMetadataFactoryProphecy->reveal(), + $iriConverterProphecy->reveal(), + $resourceClassResolverProphecy->reveal(), + $propertyAccessorProphecy->reveal(), + null, + null, + [], + $resourceMetadataCollectionFactoryProphecy->reveal(), + null, + ]); + + $serializerProphecy = $this->prophesize(SerializerInterface::class); + $serializerProphecy->willImplement(NormalizerInterface::class); + $normalizer->setSerializer($serializerProphecy->reveal()); + + $expected = [ + 'propertyCollectionIriOnlyRelation' => '/propertyCollectionIriOnlyRelations', + ]; + $this->assertSame($expected, $normalizer->normalize($propertyCollectionIriOnly, null, [ + 'resources' => [], + 'root_operation' => new GetCollection('/propertyCollectionIriOnlyRelations') + ])); + } + public function testDenormalizeWithSecuredProperty(): void { $data = [