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
15 changes: 13 additions & 2 deletions src/State/Processor/ObjectMapperProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

use ApiPlatform\Metadata\Operation;
use ApiPlatform\State\ProcessorInterface;
use ApiPlatform\State\Util\StateOptionsTrait;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

Expand All @@ -23,6 +24,8 @@
*/
final class ObjectMapperProcessor implements ProcessorInterface
{
use StateOptionsTrait;

/**
* @param ProcessorInterface<mixed,mixed> $decorated
*/
Expand All @@ -48,9 +51,17 @@ public function process(mixed $data, Operation $operation, array $uriVariables =
}

$request = $context['request'] ?? null;

// maps the Resource to an Entity
if ($request?->attributes->get('mapped_data')) {
$mappedData = $this->objectMapper->map($data, $request->attributes->get('mapped_data'));
} else {
$mappedData = $this->objectMapper->map($data, $this->getStateOptionsClass($operation, $operation->getClass()));
}
$request?->attributes->set('mapped_data', $mappedData);

$persisted = $this->decorated->process(
// maps the Resource to an Entity
$this->objectMapper->map($data, $request?->attributes->get('mapped_data')),
$mappedData,
$operation,
$uriVariables,
$context,
Expand Down
18 changes: 18 additions & 0 deletions src/Symfony/Tests/Fixtures/AnotherMappedObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?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\Symfony\Tests\Fixtures;

class AnotherMappedObject
{
}
2 changes: 2 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/FirstResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Metadata\GetCollection;
use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\SameEntity;
use Symfony\Component\ObjectMapper\Attribute\Map;

Expand All @@ -24,6 +25,7 @@
operations: [new GetCollection()],
stateOptions: new Options(entityClass: SameEntity::class)
)]
#[Map(target: AnotherMappedObject::class)]
#[Map(target: SameEntity::class)]
final class FirstResource
{
Expand Down
2 changes: 2 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/MappedResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
use ApiPlatform\Doctrine\Orm\State\Options;
use ApiPlatform\JsonLd\ContextBuilder;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject;
use ApiPlatform\Tests\Fixtures\TestBundle\Entity\MappedEntity;
use Symfony\Component\ObjectMapper\Attribute\Map;

#[ApiResource(
stateOptions: new Options(entityClass: MappedEntity::class),
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
)]
#[Map(target: AnotherMappedObject::class)]
#[Map(target: MappedEntity::class)]
final class MappedResource
{
Expand Down
2 changes: 2 additions & 0 deletions tests/Fixtures/TestBundle/ApiResource/MappedResourceOdm.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,15 @@
use ApiPlatform\Doctrine\Odm\State\Options;
use ApiPlatform\JsonLd\ContextBuilder;
use ApiPlatform\Metadata\ApiResource;
use ApiPlatform\Symfony\Tests\Fixtures\AnotherMappedObject;
use ApiPlatform\Tests\Fixtures\TestBundle\Document\MappedDocument;
use Symfony\Component\ObjectMapper\Attribute\Map;

#[ApiResource(
stateOptions: new Options(documentClass: MappedDocument::class),
normalizationContext: [ContextBuilder::HYDRA_CONTEXT_HAS_PREFIX => false],
)]
#[Map(target: AnotherMappedObject::class)]
#[Map(target: MappedDocument::class)]
final class MappedResourceOdm
{
Expand Down
33 changes: 33 additions & 0 deletions tests/Functional/MappingTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,39 @@ public function testShouldMapBetweenResourceAndEntity(): void
$this->assertResponseStatusCodeSame(204);
}

/**
* When an API resource has multiple #[Map] targets (e.g. MappedEntity + AnotherMappedObject),
* the ObjectMapperProcessor must resolve the correct target using stateOptions during POST.
*/
public function testPostWithMultipleMapTargetsResolvesCorrectEntity(): void
{
if ($this->isMongoDB()) {
$this->markTestSkipped('MongoDB not tested.');
}

if (!$this->getContainer()->has('api_platform.object_mapper')) {
$this->markTestSkipped('ObjectMapper not installed');
}

$this->recreateSchema([MappedEntity::class]);
$client = self::createClient();
$r = $client->request('POST', 'mapped_resources', ['json' => ['username' => 'multi target']]);

$this->assertResponseStatusCodeSame(201);
$this->assertJsonContains(['username' => 'multi target']);

// Verify the mapped_data is the entity from stateOptions, not AnotherMappedObject
$mappedData = $client->getKernelBrowser()->getRequest()->attributes->get('mapped_data');
$this->assertInstanceOf(MappedEntity::class, $mappedData, 'ObjectMapper should resolve to the stateOptions entity class, not the first #[Map] target.');

// Verify persistence
$repo = $this->getManager()->getRepository(MappedEntity::class);
$persisted = $repo->findOneBy(['id' => $r->toArray()['id']]);
$this->assertNotNull($persisted);
$this->assertSame('multi', $persisted->getFirstName());
$this->assertSame('target', $persisted->getLastName());
}

public function testShouldMapToTheCorrectResource(): void
{
if ($this->isMongoDB()) {
Expand Down
Loading