Skip to content

Commit 1853722

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

File tree

3 files changed

+185
-18
lines changed

3 files changed

+185
-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: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,126 @@ 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, map: true, write: 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+
map: true,
129+
write: true
130+
);
131+
132+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
133+
$objectMapper->expects($this->once())
134+
->method('map')
135+
->with($persisted, DummyOutput::class)
136+
->willReturn($output);
137+
138+
$decorated = $this->createMock(ProcessorInterface::class);
139+
$decorated->expects($this->once())
140+
->method('process')
141+
->with($entity, $operation, [], [])
142+
->willReturn($persisted);
143+
144+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
145+
$result = $processor->process($entity, $operation);
146+
147+
$this->assertSame($output, $result);
148+
}
149+
150+
public function testProcessWithCustomInputAndNoCustomOutput(): void
151+
{
152+
$input = new DummyInput();
153+
$entity = new DummyEntity();
154+
$persisted = new DummyEntity();
155+
$operation = new Post(
156+
class: DummyEntity::class,
157+
input: ['class' => DummyInput::class],
158+
map: true,
159+
write: true
160+
);
161+
162+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
163+
$objectMapper->expects($this->once())
164+
->method('map')
165+
->with($input, null)
166+
->willReturn($entity);
167+
168+
$decorated = $this->createMock(ProcessorInterface::class);
169+
$decorated->expects($this->once())
170+
->method('process')
171+
->with($entity, $operation, [], [])
172+
->willReturn($persisted);
173+
174+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
175+
$result = $processor->process($input, $operation);
176+
177+
$this->assertSame($persisted, $result);
178+
}
179+
180+
public function testProcessWithCustomInputAndCustomOutput(): void
181+
{
182+
$input = new DummyInput();
183+
$entity = new DummyEntity();
184+
$persisted = new DummyEntity();
185+
$output = new DummyOutput();
186+
$operation = new Post(
187+
class: DummyEntity::class,
188+
input: ['class' => DummyInput::class],
189+
output: ['class' => DummyOutput::class],
190+
map: true,
191+
write: true
192+
);
193+
194+
$objectMapper = $this->createMock(ObjectMapperInterface::class);
195+
$objectMapper->expects($this->exactly(2))
196+
->method('map')
197+
->willReturnCallback(function ($data, $target) use ($input, $entity, $persisted, $output) {
198+
if ($data === $input && $target === null) {
199+
return $entity;
200+
}
201+
if ($data === $persisted && $target === DummyOutput::class) {
202+
return $output;
203+
}
204+
throw new \Exception('Unexpected map call');
205+
});
206+
207+
$decorated = $this->createMock(ProcessorInterface::class);
208+
$decorated->expects($this->once())
209+
->method('process')
210+
->with($entity, $operation, [], [])
211+
->willReturn($persisted);
212+
213+
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
214+
$result = $processor->process($input, $operation);
215+
216+
$this->assertSame($output, $result);
217+
}
98218
}
99219

100220
class DummyResourceWithoutMap
@@ -105,3 +225,18 @@ class DummyResourceWithoutMap
105225
class DummyResourceWithMap
106226
{
107227
}
228+
229+
#[Map]
230+
class DummyEntity
231+
{
232+
}
233+
234+
#[Map]
235+
class DummyInput
236+
{
237+
}
238+
239+
#[Map]
240+
class DummyOutput
241+
{
242+
}

0 commit comments

Comments
 (0)