Skip to content

Commit

Permalink
Add source, destination and property stacks in context array
Browse files Browse the repository at this point in the history
  • Loading branch information
Toilal committed Aug 8, 2019
1 parent 88145c3 commit f2c2c7d
Show file tree
Hide file tree
Showing 3 changed files with 132 additions and 38 deletions.
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -833,12 +833,19 @@ $mapper->map($employee, EmployeeDto::class, ['locale' => $request->getLocale()])
```

When using the `mapToObject` method, the context will contain the destination
object by default. It is accessible using `$context['AutoMapper::DESTINATION_CONTEXT']`.
object by default. It is accessible using `$context[AutoMapper::DESTINATION_CONTEXT]`.
This is useful in scenarios where you need data from the destination object
to populate the object you're mapping.

When implementing a custom constructor, the context will contain the destination
class by default. It is accessible using `$context['AutoMapper::DESTINATION_CLASS_CONTEXT']`.
class by default. It is accessible using `$context[AutoMapper::DESTINATION_CLASS_CONTEXT]`.

When mapping an object graph, the context will also contain arrays for property
name paths, ancestor source objects and ancestor destination objects. Those arrays
are accessible using `$context[AutoMapper::PROPERTY_STACK_CONTEXT]`,
`$context[AutoMapper::SOURCE_STACK_CONTEXT]` and `$context[AutoMapper::DESTINATION_STACK_CONTEXT]`.
They can be used to implement custom mapping function based on the hierarchy level and current position
inside the object graph being mapped.

### Misc

Expand Down
108 changes: 73 additions & 35 deletions src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@
*/
class AutoMapper implements AutoMapperInterface
{
public const SOURCE_STACK_CONTEXT = '__source_stack';
public const DESTINATION_STACK_CONTEXT = '__destination_stack';
public const PROPERTY_STACK_CONTEXT = '__property_stack';
public const DESTINATION_CONTEXT = '__destination';
public const DESTINATION_CLASS_CONTEXT = '__destination_class';

Expand Down Expand Up @@ -48,6 +51,22 @@ public static function initialize(callable $configurator): AutoMapperInterface
return $mapper;
}

private function push($key, $value, &$context)
{
if (!array_key_exists($key, $context)) {
$stack = [];
} else {
$stack = $context[$key];
}
$stack[] = $value;
$context[$key] = $stack;
}

private function pop($key, &$context)
{
array_pop($context[$key]);
}

/**
* @inheritdoc
*/
Expand All @@ -59,8 +78,7 @@ public function map($source, string $destinationClass, array $context = [])

if (\is_object($source)) {
$sourceClass = \get_class($source);
}
else {
} else {
$sourceClass = \gettype($source);
if ($sourceClass !== DataType::ARRAY) {
throw UnsupportedSourceTypeException::fromType($sourceClass);
Expand All @@ -82,21 +100,27 @@ public function map($source, string $destinationClass, array $context = [])
$this,
$context
);
}
elseif (interface_exists($destinationClass)) {
} elseif (interface_exists($destinationClass)) {
// If we're mapping to an interface a valid custom constructor has
// to be provided. Otherwise we can't know what to do.
$message = 'Mapping to an interface is not possible. Please '
. 'provide a concrete class or use mapToObject instead.';
throw new AutoMapperPlusException($message);
}
else {
} else {
$destinationObject = new $destinationClass;
}

$context[self::DESTINATION_CONTEXT] = $destinationObject;

return $this->doMap($source, $destinationObject, $mapping, $context);
$this->push(self::SOURCE_STACK_CONTEXT, $source, $context);
$this->push(self::DESTINATION_STACK_CONTEXT, $destinationObject, $context);

try {
return $this->doMap($source, $destinationObject, $mapping, $context);
} finally {
$this->pop(self::DESTINATION_STACK_CONTEXT, $context);
$this->pop(self::SOURCE_STACK_CONTEXT, $context);
}
}

/**
Expand All @@ -106,8 +130,9 @@ public function mapMultiple(
$sourceCollection,
string $destinationClass,
array $context = []
): array {
if(!is_iterable($sourceCollection)){
): array
{
if (!is_iterable($sourceCollection)) {
throw new InvalidArgumentException(
'The collection provided should be iterable.'
);
Expand All @@ -128,8 +153,7 @@ public function mapToObject($source, $destination, array $context = [])
{
if (\is_object($source)) {
$sourceClass = \get_class($source);
}
else {
} else {
$sourceClass = \gettype($source);
if ($sourceClass !== DataType::ARRAY) {
throw UnsupportedSourceTypeException::fromType($sourceClass);
Expand All @@ -146,21 +170,28 @@ public function mapToObject($source, $destination, array $context = [])
$context
);

$mapping = $this->getMapping($sourceClass, $destinationClass);
if ($mapping->providesCustomMapper()) {
return $this->getCustomMapper($mapping)->mapToObject(
$this->push(self::SOURCE_STACK_CONTEXT, $source, $context);
$this->push(self::DESTINATION_STACK_CONTEXT, $destination, $context);
try {
$mapping = $this->getMapping($sourceClass, $destinationClass);
if ($mapping->providesCustomMapper()) {
return $this->getCustomMapper($mapping)->mapToObject(
$source,
$destination,
$context
);
}

return $this->doMap(
$source,
$destination,
$mapping,
$context
);
} finally {
$this->pop(self::DESTINATION_STACK_CONTEXT, $context);
$this->pop(self::SOURCE_STACK_CONTEXT, $context);
}

return $this->doMap(
$source,
$destination,
$mapping,
$context
);
}

/**
Expand All @@ -178,23 +209,29 @@ protected function doMap(
$destination,
MappingInterface $mapping,
array $context = []
) {
)
{
$propertyNames = $mapping->getTargetProperties($destination, $source);
foreach ($propertyNames as $propertyName) {
$mappingOperation = $mapping->getMappingOperationFor($propertyName);
$this->push(self::PROPERTY_STACK_CONTEXT, $propertyName, $context);
try {
$mappingOperation = $mapping->getMappingOperationFor($propertyName);

if ($mappingOperation instanceof MapperAwareOperation) {
$mappingOperation->setMapper($this);
}
if ($mappingOperation instanceof ContextAwareOperation) {
$mappingOperation->setContext($context);
}
if ($mappingOperation instanceof MapperAwareOperation) {
$mappingOperation->setMapper($this);
}
if ($mappingOperation instanceof ContextAwareOperation) {
$mappingOperation->setContext($context);
}

$mappingOperation->mapProperty(
$propertyName,
$source,
$destination
);
$mappingOperation->mapProperty(
$propertyName,
$source,
$destination
);
} finally {
$this->pop(self::PROPERTY_STACK_CONTEXT, $context);
}
}

return $destination;
Expand All @@ -218,7 +255,8 @@ protected function getMapping
(
string $sourceClass,
string $destinationClass
): MappingInterface {
): MappingInterface
{
$mapping = $this->autoMapperConfig->getMappingFor(
$sourceClass,
$destinationClass
Expand Down
51 changes: 50 additions & 1 deletion test/Scenarios/ContextTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
use AutoMapperPlus\Configuration\AutoMapperConfig;
use AutoMapperPlus\MappingOperation\Implementations\MapTo;
use AutoMapperPlus\MappingOperation\Operation;
use AutoMapperPlus\Test\Models\Nested\ChildClass;
use AutoMapperPlus\Test\Models\Nested\ChildClassDto;
use AutoMapperPlus\Test\Models\Nested\ParentClass;
use AutoMapperPlus\Test\Models\Nested\ParentClassDto;
use AutoMapperPlus\Test\Models\SimpleProperties\Destination;
use AutoMapperPlus\Test\Models\SimpleProperties\Source;
use PHPUnit\Framework\TestCase;
Expand Down Expand Up @@ -60,7 +64,7 @@ public function testContextCanBePassedToMapMultiple()
{
$config = new AutoMapperConfig();
$config->registerMapping(Source::class, Destination::class)
->forMember('name', function ($source, $mapper, $context= []) {
->forMember('name', function ($source, $mapper, $context = []) {
$this->assertArrayHasKey('context_key', $context);
$this->assertEquals('context-value', $context['context_key']);

Expand Down Expand Up @@ -162,4 +166,49 @@ public function testDestinationClassIsPassed()
Destination::class
);
}

public function testMapToBuildsContextStacks()
{
$parent = new ParentClass();
$parent->child = new ChildClass();

$config = new AutoMapperConfig();
$config->registerMapping(ParentClass::class, ParentClassDto::class)
->forMember('child', Operation::mapTo(ChildClassDto::class));

$config->registerMapping(ChildClass::class, ChildClassDto::class)
->forMember('name', function ($source, $mapper, $context = []) use ($parent) {
$this->assertArrayHasKey(
AutoMapper::SOURCE_STACK_CONTEXT,
$context
);

$this->assertEquals([$parent, $parent->child], $context[AutoMapper::SOURCE_STACK_CONTEXT]);

$this->assertArrayHasKey(
AutoMapper::DESTINATION_STACK_CONTEXT,
$context
);

$this->assertCount(2, $context[AutoMapper::DESTINATION_STACK_CONTEXT]);
$this->assertInstanceOf(ParentClassDto::class, $context[AutoMapper::DESTINATION_STACK_CONTEXT][0]);
$this->assertInstanceOf(ChildClassDto::class, $context[AutoMapper::DESTINATION_STACK_CONTEXT][1]);

$this->assertEquals($context[AutoMapper::DESTINATION_CONTEXT], $context[AutoMapper::DESTINATION_STACK_CONTEXT][1]);

$this->assertArrayHasKey(
AutoMapper::PROPERTY_STACK_CONTEXT,
$context
);

$this->assertEquals($context[AutoMapper::PROPERTY_STACK_CONTEXT], ['child', 'name']);
});

$mapper = new AutoMapper($config);

$mapper->map(
$parent,
ParentClassDto::class
);
}
}

0 comments on commit f2c2c7d

Please sign in to comment.