Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public function create(string $resourceClass, string $property, array $options =
// on output the serializer embeds the relation as soon as gen_id is false, even when it is not a readable link (see AbstractItemNormalizer::normalizeRelation())
$link = $isInput ? $propertyMetadata->isWritableLink() : ($propertyMetadata->isReadableLink() || false === $propertyMetadata->getGenId());

// on output a non-resource object is serialized by the standard object normalizer, which embeds related resources regardless of readableLink (see AbstractItemNormalizer::supportsNormalization())
if (!$isInput && !$this->isResourceClass($resourceClass)) {
$link = true;
}

$propertySchema = $propertyMetadata->getSchema() ?? [];

if (null !== $propertyMetadata->getUriTemplate() || (!\array_key_exists('readOnly', $propertySchema) && false === $propertyMetadata->isWritable() && !$propertyMetadata->isInitializable())) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,59 @@ public function testRelationWithGenIdFalseIsEmbeddedInOutputSchema(): void
$this->assertSame(['type' => Schema::UNKNOWN_TYPE], $apiProperty->getSchema());
}

/**
* A relation borne by a non-resource object (e.g. a raw Doctrine entity embedded as a readable link)
* is serialized by the standard object normalizer, which embeds the related resource regardless of its
* readableLink/genId. The output schema must embed it too, otherwise a strict client rejects the payload.
*/
public function testRelationOnNonResourceParentIsEmbeddedInOutputSchema(): void
{
if (!method_exists(PropertyInfoExtractor::class, 'getType')) { // @phpstan-ignore-line symfony/property-info 6.4 is still allowed and this may be true
$this->markTestSkipped('This test only supports type-info component');
}

$resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
// the parent (DummyWithEnum) is not a resource, the related class (Dummy) is
$resourceClassResolver->method('isResourceClass')->willReturnCallback(static fn (string $class): bool => Dummy::class === $class);

// not a readable link, gen_id left to its default: the relation would normally be an iri-reference string
$apiProperty = (new ApiProperty(nativeType: Type::object(Dummy::class)))
->withReadableLink(false);
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
$decorated->expects($this->once())->method('create')->with(DummyWithEnum::class, 'relatedDummy')->willReturn($apiProperty);

$schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated);
$apiProperty = $schemaPropertyMetadataFactory->create(DummyWithEnum::class, 'relatedDummy');

// defers to SchemaFactory ($ref to embedded subschema) instead of an iri-reference string
$this->assertSame(['type' => Schema::UNKNOWN_TYPE], $apiProperty->getSchema());
}

/**
* Counterpart to the non-resource case: a non-readable-link relation on a *resource* parent still follows
* readableLink and stays an iri-reference string — the non-resource guard must not widen to resources.
*/
public function testRelationOnResourceParentStaysIriReference(): void
{
if (!method_exists(PropertyInfoExtractor::class, 'getType')) { // @phpstan-ignore-line symfony/property-info 6.4 is still allowed and this may be true
$this->markTestSkipped('This test only supports type-info component');
}

$resourceClassResolver = $this->createMock(ResourceClassResolverInterface::class);
// both the parent and the related class are resources
$resourceClassResolver->method('isResourceClass')->willReturn(true);

$apiProperty = (new ApiProperty(nativeType: Type::object(Dummy::class)))
->withReadableLink(false);
$decorated = $this->createMock(PropertyMetadataFactoryInterface::class);
$decorated->expects($this->once())->method('create')->with(Dummy::class, 'relatedDummy')->willReturn($apiProperty);

$schemaPropertyMetadataFactory = new SchemaPropertyMetadataFactory($resourceClassResolver, $decorated);
$apiProperty = $schemaPropertyMetadataFactory->create(Dummy::class, 'relatedDummy');

$this->assertSame(['type' => 'string', 'format' => 'iri-reference', 'example' => 'https://example.com/'], $apiProperty->getSchema());
}

public function testMixed(): void
{
if (!method_exists(PropertyInfoExtractor::class, 'getType')) { // @phpstan-ignore-line symfony/property-info 6.4 is still allowed and this may be true
Expand Down
Loading