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 @@ -52,11 +52,14 @@ public function create(string $resourceClass): ResourceMetadataCollection
$entityClass = $options->getDocumentClass();
}

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

// Look for Mapping metadata
if ($this->canBeMapped($class) || ($entityClass && ($entityMap = $this->canBeMapped($entityClass)))) {
if ($this->canBeMapped($inputClass)
|| ($outputClass && $this->canBeMapped($outputClass))
|| ($entityClass && ($entityMap = $this->canBeMapped($entityClass)))) {
$found = true;
if ($entityMap) {
foreach ($entityMap as $mapping) {
Expand Down
61 changes: 45 additions & 16 deletions src/State/Processor/ObjectMapperProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,35 +34,64 @@ public function __construct(

public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): object|array|null
{
$class = $operation->getInput()['class'] ?? $operation->getClass();

if (
$data instanceof Response
|| !$this->objectMapper
|| !$operation->canWrite()
|| null === $data
|| !is_a($data, $class, true)
|| !$operation->canMap()
) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

$request = $context['request'] ?? null;
$persisted = $this->decorated->process(
// maps the Resource to an Entity
$this->objectMapper->map($data, $request?->attributes->get('mapped_data')),
$operation,
$uriVariables,
$context,
);
$resourceClass = $operation->getClass();
$inputClass = $operation->getInput()['class'] ?? null;
$outputClass = $operation->getOutput()['class'] ?? null;

// Get entity class from state options if available
$stateOptions = $operation->getStateOptions();
$entityClass = null;
if ($stateOptions) {
if (method_exists($stateOptions, 'getEntityClass')) {
$entityClass = $stateOptions->getEntityClass();
} elseif (method_exists($stateOptions, 'getDocumentClass')) {
$entityClass = $stateOptions->getDocumentClass();
}
}

$hasCustomInput = null !== $inputClass && $inputClass !== $resourceClass;
$hasCustomOutput = null !== $outputClass && $outputClass !== $resourceClass;
$hasEntityMapping = null !== $entityClass && $entityClass !== $resourceClass;

// Skip mapping if no custom input/output and no entity mapping needed
if (!$hasCustomInput && !$hasCustomOutput && !$hasEntityMapping) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

// Map input to entity if we have custom input or entity mapping
if ($hasCustomInput || $hasEntityMapping) {
$expectedInputClass = $hasCustomInput ? $inputClass : $resourceClass;
if (!is_a($data, $expectedInputClass, true)) {
return $this->decorated->process($data, $operation, $uriVariables, $context);
}

$data = $this->objectMapper->map($data, $request?->attributes->get('mapped_data'));
}

$persisted = $this->decorated->process($data, $operation, $uriVariables, $context);
$request?->attributes->set('persisted_data', $persisted);

// return the Resource representation of the persisted entity
return $this->objectMapper->map(
// persist the entity
$persisted,
$operation->getClass()
);
// Map output back to resource or custom output class
if ($hasCustomOutput) {
return $this->objectMapper->map($persisted, $outputClass);
}

// If we have entity mapping but no custom output, map back to resource class
if ($hasEntityMapping) {
return $this->objectMapper->map($persisted, $resourceClass);
}

return $persisted;
}
}
160 changes: 160 additions & 0 deletions src/State/Tests/Processor/ObjectMapperProcessorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,151 @@ public function testProcessBypassesWithoutMapAttribute(): void
$processor = new ObjectMapperProcessor($objectMapper, $decorated);
$this->assertEquals($data, $processor->process($data, $operation));
}

public function testProcessWithNoCustomInputAndNoCustomOutput(): void
{
$this->skipIfMapParameterNotAvailable();

$entity = new DummyEntity();
$persisted = new DummyEntity();
$operation = new Post(class: DummyEntity::class, map: true, write: true);

$objectMapper = $this->createMock(ObjectMapperInterface::class);
$objectMapper->expects($this->never())->method('map');

$decorated = $this->createMock(ProcessorInterface::class);
$decorated->expects($this->once())
->method('process')
->with($entity, $operation, [], [])
->willReturn($persisted);

$processor = new ObjectMapperProcessor($objectMapper, $decorated);
$result = $processor->process($entity, $operation);

$this->assertSame($persisted, $result);
}

public function testProcessWithNoCustomInputAndCustomOutput(): void
{
$this->skipIfMapParameterNotAvailable();

$entity = new DummyEntity();
$persisted = new DummyEntity();
$output = new DummyOutput();
$operation = new Post(
class: DummyEntity::class,
output: ['class' => DummyOutput::class],
map: true,
write: true
);

$objectMapper = $this->createMock(ObjectMapperInterface::class);
$objectMapper->expects($this->once())
->method('map')
->with($persisted, DummyOutput::class)
->willReturn($output);

$decorated = $this->createMock(ProcessorInterface::class);
$decorated->expects($this->once())
->method('process')
->with($entity, $operation, [], [])
->willReturn($persisted);

$processor = new ObjectMapperProcessor($objectMapper, $decorated);
$result = $processor->process($entity, $operation);

$this->assertSame($output, $result);
}

public function testProcessWithCustomInputAndNoCustomOutput(): void
{
$this->skipIfMapParameterNotAvailable();

$input = new DummyInput();
$entity = new DummyEntity();
$persisted = new DummyEntity();
$operation = new Post(
class: DummyEntity::class,
input: ['class' => DummyInput::class],
map: true,
write: true
);

$objectMapper = $this->createMock(ObjectMapperInterface::class);
$objectMapper->expects($this->once())
->method('map')
->with($input, null)
->willReturn($entity);

$decorated = $this->createMock(ProcessorInterface::class);
$decorated->expects($this->once())
->method('process')
->with($entity, $operation, [], [])
->willReturn($persisted);

$processor = new ObjectMapperProcessor($objectMapper, $decorated);
$result = $processor->process($input, $operation);

$this->assertSame($persisted, $result);
}

public function testProcessWithCustomInputAndCustomOutput(): void
{
$this->skipIfMapParameterNotAvailable();

$input = new DummyInput();
$entity = new DummyEntity();
$persisted = new DummyEntity();
$output = new DummyOutput();
$operation = new Post(
class: DummyEntity::class,
input: ['class' => DummyInput::class],
output: ['class' => DummyOutput::class],
map: true,
write: true
);

$objectMapper = $this->createMock(ObjectMapperInterface::class);
$objectMapper->expects($this->exactly(2))
->method('map')
->willReturnCallback(function ($data, $target) use ($input, $entity, $persisted, $output) {
if ($data === $input && null === $target) {
return $entity;
}
if ($data === $persisted && DummyOutput::class === $target) {
return $output;
}
throw new \Exception('Unexpected map call');
});

$decorated = $this->createMock(ProcessorInterface::class);
$decorated->expects($this->once())
->method('process')
->with($entity, $operation, [], [])
->willReturn($persisted);

$processor = new ObjectMapperProcessor($objectMapper, $decorated);
$result = $processor->process($input, $operation);

$this->assertSame($output, $result);
}

private function skipIfMapParameterNotAvailable(): void
{
try {
$reflection = new \ReflectionClass(Post::class);
$constructor = $reflection->getConstructor();
$parameters = $constructor->getParameters();
foreach ($parameters as $parameter) {
if ('map' === $parameter->getName()) {
return;
}
}
$this->markTestSkipped('The "map" parameter is not available in this version');
} catch (\ReflectionException $e) {
$this->markTestSkipped('Could not check for "map" parameter availability');
}
}
}

class DummyResourceWithoutMap
Expand All @@ -105,3 +250,18 @@ class DummyResourceWithoutMap
class DummyResourceWithMap
{
}

#[Map]
class DummyEntity
{
}

#[Map]
class DummyInput
{
}

#[Map]
class DummyOutput
{
}
Loading