Skip to content
Merged
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
2 changes: 2 additions & 0 deletions src/Serializer/AbstractItemNormalizer.php
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,8 @@ public function denormalize(mixed $data, string $type, ?string $format = null, a
throw NotNormalizableValueException::createForUnexpectedDataType(\sprintf('The type of the "%s" resource must be "array" (nested document) or "string" (IRI), "%s" given.', $resourceClass, \gettype($data)), $data, ['array', 'string'], $context['deserialization_path'] ?? null);
}

unset($context['relation_native_type']);

$previousObject = $this->clone($objectToPopulate);
$object = parent::denormalize($data, $type, $format, $context);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Operation;

#[Get(
shortName: 'EmbeddedRelationChild',
uriTemplate: '/embedded_relation_children/{id}',
provider: [self::class, 'provide'],
)]
class EmbeddedRelationChild
{
#[ApiProperty(identifier: true)]
public int $id = 1;

/**
* @var EmbeddedRelationRow[]
*/
public array $rows = [];

public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self
{
$child = new self();
$child->id = (int) ($uriVariables['id'] ?? 1);

return $child;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Post;

#[Post(
shortName: 'EmbeddedRelationParent',
uriTemplate: '/embedded_relation_parents',
processor: [self::class, 'process'],
)]
class EmbeddedRelationParent
{
#[ApiProperty(identifier: true)]
public int $id = 1;

/**
* @var EmbeddedRelationChild[]
*/
#[ApiProperty(writableLink: true, readableLink: true)]
public array $items = [];

public static function process(mixed $data): self
{
return $data;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType;

final class EmbeddedRelationRow
{
public ?EmbeddedRelationTarget $ref = null;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType;

use ApiPlatform\Metadata\ApiProperty;
use ApiPlatform\Metadata\Get;
use ApiPlatform\Metadata\Operation;

#[Get(
shortName: 'EmbeddedRelationTarget',
uriTemplate: '/embedded_relation_targets/{id}',
provider: [self::class, 'provide'],
)]
class EmbeddedRelationTarget
{
#[ApiProperty(identifier: true)]
public int $id = 1;

public static function provide(Operation $operation, array $uriVariables = [], array $context = []): self
{
$target = new self();
$target->id = (int) ($uriVariables['id'] ?? 1);

return $target;
}
}
76 changes: 76 additions & 0 deletions tests/Functional/Serializer/EmbeddedRelationNativeTypeTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* 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\Tests\Functional\Serializer;

use ApiPlatform\Symfony\Bundle\Test\ApiTestCase;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType\EmbeddedRelationChild;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType\EmbeddedRelationParent;
use ApiPlatform\Tests\Fixtures\TestBundle\ApiResource\EmbeddedRelationNativeType\EmbeddedRelationTarget;
use ApiPlatform\Tests\SetupClassResourcesTrait;
use Symfony\Component\PropertyInfo\PropertyInfoExtractor;

/**
* The "relation_native_type" context key set for the embedded EmbeddedRelationChild collection must not
* leak into the nested plain DTO: the IRI type-confusion guard in getResourceFromIri() would otherwise
* compare the resolved EmbeddedRelationTarget against EmbeddedRelationChild and reject a valid IRI.
*/
final class EmbeddedRelationNativeTypeTest extends ApiTestCase
{
use SetupClassResourcesTrait;

protected static ?bool $alwaysBootKernel = false;

/**
* @return class-string[]
*/
public static function getResources(): array
{
// EmbeddedRelationRow is a plain DTO on purpose and is not registered as a resource.
return [EmbeddedRelationParent::class, EmbeddedRelationChild::class, EmbeddedRelationTarget::class];
}

public function testIriInsideEmbeddedDocumentIsNotValidatedAgainstTheOuterRelationType(): void
{
// relation_native_type is only set on the native-type path; the legacy property-info path never triggers the bug.
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$this->markTestSkipped('Requires symfony/property-info >= 7.1 (native types).');
}

$response = self::createClient()->request('POST', '/embedded_relation_parents', [
'headers' => ['Content-Type' => 'application/ld+json', 'Accept' => 'application/ld+json'],
'json' => ['items' => [['rows' => [['ref' => '/embedded_relation_targets/1']]]]],
]);

$this->assertResponseStatusCodeSame(201);
// EmbeddedRelationRow is not a resource, so its $ref is rendered by the generic object normalizer as a
// nested document rather than collapsed to an IRI string; asserting on "@id" pins the resolved target.
$this->assertJsonContains(['items' => [['rows' => [['ref' => ['@id' => '/embedded_relation_targets/1']]]]]]);
}

public function testMismatchedIriInsideEmbeddedDocumentIsStillRejected(): void
{
if (!method_exists(PropertyInfoExtractor::class, 'getType')) {
$this->markTestSkipped('Requires symfony/property-info >= 7.1 (native types).');
}

// An IRI of the wrong resource class in the same nested position must still hit the type guard.
self::createClient()->request('POST', '/embedded_relation_parents', [
'headers' => ['Content-Type' => 'application/ld+json', 'Accept' => 'application/ld+json'],
'json' => ['items' => [['rows' => [['ref' => '/embedded_relation_children/1']]]]],
]);

$this->assertResponseStatusCodeSame(400);
$this->assertJsonContains(['detail' => 'Invalid IRI "/embedded_relation_children/1".']);
}
}
Loading