Skip to content

Commit 0587ff6

Browse files
committed
fix(metadata): enhance resource class determination before Object Mapper Processor return
1 parent 2a34498 commit 0587ff6

File tree

3 files changed

+182
-18
lines changed

3 files changed

+182
-18
lines changed

src/Metadata/Resource/Factory/ObjectMapperMetadataCollectionFactory.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,14 @@ public function create(string $resourceClass): ResourceMetadataCollection
5252
$entityClass = $options->getDocumentClass();
5353
}
5454

55-
$class = $operation->getInput()['class'] ?? $operation->getClass();
55+
$inputClass = $operation->getInput()['class'] ?? $operation->getClass();
56+
$outputClass = $operation->getOutput()['class'] ?? null;
5657
$entityMap = null;
5758

5859
// Look for Mapping metadata
59-
if ($this->canBeMapped($class) || ($entityClass && ($entityMap = $this->canBeMapped($entityClass)))) {
60+
if ($this->canBeMapped($inputClass)
61+
|| ($outputClass && $this->canBeMapped($outputClass))
62+
|| ($entityClass && ($entityMap = $this->canBeMapped($entityClass)))) {
6063
$found = true;
6164
if ($entityMap) {
6265
foreach ($entityMap as $mapping) {

src/State/Processor/ObjectMapperProcessor.php

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,35 +34,64 @@ public function __construct(
3434

3535
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
3636
{
37-
$class = $operation->getInput()['class'] ?? $operation->getClass();
38-
3937
if (
4038
$data instanceof Response
4139
|| !$this->objectMapper
4240
|| !$operation->canWrite()
4341
|| null === $data
44-
|| !is_a($data, $class, true)
4542
|| !$operation->canMap()
4643
) {
4744
return $this->decorated->process($data, $operation, $uriVariables, $context);
4845
}
4946

5047
$request = $context['request'] ?? null;
51-
$persisted = $this->decorated->process(
52-
// maps the Resource to an Entity
53-
$this->objectMapper->map($data, $request?->attributes->get('mapped_data')),
54-
$operation,
55-
$uriVariables,
56-
$context,
57-
);
48+
$resourceClass = $operation->getClass();
49+
$inputClass = $operation->getInput()['class'] ?? null;
50+
$outputClass = $operation->getOutput()['class'] ?? null;
51+
52+
// Get entity class from state options if available
53+
$stateOptions = $operation->getStateOptions();
54+
$entityClass = null;
55+
if ($stateOptions) {
56+
if (method_exists($stateOptions, 'getEntityClass')) {
57+
$entityClass = $stateOptions->getEntityClass();
58+
} elseif (method_exists($stateOptions, 'getDocumentClass')) {
59+
$entityClass = $stateOptions->getDocumentClass();
60+
}
61+
}
62+
63+
$hasCustomInput = null !== $inputClass && $inputClass !== $resourceClass;
64+
$hasCustomOutput = null !== $outputClass && $outputClass !== $resourceClass;
65+
$hasEntityMapping = null !== $entityClass && $entityClass !== $resourceClass;
66+
67+
// Skip mapping if no custom input/output and no entity mapping needed
68+
if (!$hasCustomInput && !$hasCustomOutput && !$hasEntityMapping) {
69+
return $this->decorated->process($data, $operation, $uriVariables, $context);
70+
}
5871

72+
// Map input to entity if we have custom input or entity mapping
73+
if ($hasCustomInput || $hasEntityMapping) {
74+
$expectedInputClass = $hasCustomInput ? $inputClass : $resourceClass;
75+
if (!is_a($data, $expectedInputClass, true)) {
76+
return $this->decorated->process($data, $operation, $uriVariables, $context);
77+
}
78+
79+
$data = $this->objectMapper->map($data, $request?->attributes->get('mapped_data'));
80+
}
81+
82+
$persisted = $this->decorated->process($data, $operation, $uriVariables, $context);
5983
$request?->attributes->set('persisted_data', $persisted);
6084

61-
// return the Resource representation of the persisted entity
62-
return $this->objectMapper->map(
63-
// persist the entity
64-
$persisted,
65-
$operation->getClass()
66-
);
85+
// Map output back to resource or custom output class
86+
if ($hasCustomOutput) {
87+
return $this->objectMapper->map($persisted, $outputClass);
88+
}
89+
90+
// If we have entity mapping but no custom output, map back to resource class
91+
if ($hasEntityMapping) {
92+
return $this->objectMapper->map($persisted, $resourceClass);
93+
}
94+
95+
return $persisted;
6796
}
6897
}

src/State/Tests/Processor/ObjectMapperProcessorTest.php

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,123 @@ public function testProcessBypassesWithoutMapAttribute(): void
9595
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
9696
$this->assertEquals($data, $processor->process($data, $operation));
9797
}
98+
99+
public function testProcessWithNoCustomInputAndNoCustomOutput(): void
100+
{
101+
$entity = new DummyEntity();
102+
$persisted = new DummyEntity();
103+
$operation = (new Post(class: DummyEntity::class, write: true))->withMap(true);
104+
105+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
106+
$objectMapper->expects($this->never())->method('map');
107+
108+
$decorated = $this->createMock(ProcessorInterface::class);
109+
$decorated->expects($this->once())
110+
->method('process')
111+
->with($entity, $operation, [], [])
112+
->willReturn($persisted);
113+
114+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
115+
$result = $processor->process($entity, $operation);
116+
117+
$this->assertSame($persisted, $result);
118+
}
119+
120+
public function testProcessWithNoCustomInputAndCustomOutput(): void
121+
{
122+
$entity = new DummyEntity();
123+
$persisted = new DummyEntity();
124+
$output = new DummyOutput();
125+
$operation = (new Post(
126+
class: DummyEntity::class,
127+
output: ['class' => DummyOutput::class],
128+
write: true
129+
))->withMap(true);
130+
131+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
132+
$objectMapper->expects($this->once())
133+
->method('map')
134+
->with($persisted, DummyOutput::class)
135+
->willReturn($output);
136+
137+
$decorated = $this->createMock(ProcessorInterface::class);
138+
$decorated->expects($this->once())
139+
->method('process')
140+
->with($entity, $operation, [], [])
141+
->willReturn($persisted);
142+
143+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
144+
$result = $processor->process($entity, $operation);
145+
146+
$this->assertSame($output, $result);
147+
}
148+
149+
public function testProcessWithCustomInputAndNoCustomOutput(): void
150+
{
151+
$input = new DummyInput();
152+
$entity = new DummyEntity();
153+
$persisted = new DummyEntity();
154+
$operation = (new Post(
155+
class: DummyEntity::class,
156+
input: ['class' => DummyInput::class],
157+
write: true
158+
))->withMap(true);
159+
160+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
161+
$objectMapper->expects($this->once())
162+
->method('map')
163+
->with($input, null)
164+
->willReturn($entity);
165+
166+
$decorated = $this->createMock(ProcessorInterface::class);
167+
$decorated->expects($this->once())
168+
->method('process')
169+
->with($entity, $operation, [], [])
170+
->willReturn($persisted);
171+
172+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
173+
$result = $processor->process($input, $operation);
174+
175+
$this->assertSame($persisted, $result);
176+
}
177+
178+
public function testProcessWithCustomInputAndCustomOutput(): void
179+
{
180+
$input = new DummyInput();
181+
$entity = new DummyEntity();
182+
$persisted = new DummyEntity();
183+
$output = new DummyOutput();
184+
$operation = (new Post(
185+
class: DummyEntity::class,
186+
input: ['class' => DummyInput::class],
187+
output: ['class' => DummyOutput::class],
188+
write: true
189+
))->withMap(true);
190+
191+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
192+
$objectMapper->expects($this->exactly(2))
193+
->method('map')
194+
->willReturnCallback(function ($data, $target) use ($input, $entity, $persisted, $output) {
195+
if ($data === $input && null === $target) {
196+
return $entity;
197+
}
198+
if ($data === $persisted && DummyOutput::class === $target) {
199+
return $output;
200+
}
201+
throw new \Exception('Unexpected map call');
202+
});
203+
204+
$decorated = $this->createMock(ProcessorInterface::class);
205+
$decorated->expects($this->once())
206+
->method('process')
207+
->with($entity, $operation, [], [])
208+
->willReturn($persisted);
209+
210+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
211+
$result = $processor->process($input, $operation);
212+
213+
$this->assertSame($output, $result);
214+
}
98215
}
99216

100217
class DummyResourceWithoutMap
@@ -105,3 +222,18 @@ class DummyResourceWithoutMap
105222
class DummyResourceWithMap
106223
{
107224
}
225+
226+
#[Map]
227+
class DummyEntity
228+
{
229+
}
230+
231+
#[Map]
232+
class DummyInput
233+
{
234+
}
235+
236+
#[Map]
237+
class DummyOutput
238+
{
239+
}

0 commit comments

Comments
 (0)