diff --git a/README.md b/README.md
index afeb95f..68b1156 100644
--- a/README.md
+++ b/README.md
@@ -216,6 +216,7 @@ The following operations are provided:
| MapTo | Maps the property to another class. Allows for [nested mappings](#dealing-with-nested-mappings). Supports both single values and collections. |
| FromProperty | Use this to explicitly state the source property name. |
| DefaultMappingOperation | Simply transfers the property, taking into account the provided naming conventions (if there are any). |
+| MapFromWithMapper | Similar to MapFrom.
Compared to `mapFrom`, the callback has access to a instance of `AutoMapperInterface`. Define the second callback argument of `AutoMapperInterface` type. Accessible by using
- `Operation::mapFromWithMapper(function($source, AutoMapperInterface $mapper){ ... })`
- `new mapFromWithMapper(function($source, AutoMapperInterface $mapper){ ... })`
You can use them with the same `forMember()` method. The `Operation` class can
be used for clarity.
@@ -238,6 +239,35 @@ $mapping->forMember('id', Operation::ignore());
$mapping->forMember('employee', Operation::mapTo(EmployeeDto::class));
// Explicitly state what the property name is of the source object.
$mapping->forMember('name', Operation::fromProperty('unconventially_named_property'));
+// The `FromProperty` operation can be chained with `MapTo`, allowing a
+// differently named property to be mapped to a class.
+$mapping->forMember(
+ 'address',
+ Operation::fromProperty('adres')->mapTo(Address::class)
+);
+```
+
+Example of using `MapFromWithMapper`:
+
+```php
+xpath('/product/specification/palette/colour');
+
+ return $mapper->mapMultiple($palette, Color::class);
+};
+
+$mapping->forMember('palette', Operation::mapFromWithMapper($getColorPalette));
+
+// Or another Example using inline function
+$mapping->forMember('palette', new MapFromWithMapper(function(SimpleXMLElement $XMLElement, AutoMapperInterface $mapper) {
+ /** @var SimpleXMLElement $palette */
+ $palette = $XMLElement->xpath('/product/specification/palette/colour');
+
+ return $mapper->mapMultiple($palette, Color::class);
+}));
```
You can create your own operations by implementing the
@@ -245,6 +275,10 @@ You can create your own operations by implementing the
[provided implementations](https://github.com/mark-gerarts/automapper-plus/tree/master/src/MappingOperation)
for some inspiration.
+If you need to have the automapper available in your operation, you can
+implement the `MapperAwareInterface`, and use the `MapperAwareTrait`. The
+default `MapTo` and `MapFromWithMapper` operations use these.
+
#### Dealing with nested mappings
Nested mappings can be registered using the `MapTo` operation. Keep in mind that the mapping for the
child class has to be registered as well.
@@ -756,3 +790,4 @@ Collection size: 10000
- [ ] Clean up the property checking in the Mapping::forMember() method.
- [ ] Refactor tests
- [ ] Allow setting a maximum depth, see #14
+- [ ] Provide a NameResolver that accepts an array mapping, as an alternative to multiple `FromProperty`s
diff --git a/src/AutoMapper.php b/src/AutoMapper.php
index f7987f6..f1d0c38 100644
--- a/src/AutoMapper.php
+++ b/src/AutoMapper.php
@@ -6,7 +6,7 @@
use AutoMapperPlus\Configuration\AutoMapperConfigInterface;
use AutoMapperPlus\Configuration\MappingInterface;
use AutoMapperPlus\Exception\UnregisteredMappingException;
-use AutoMapperPlus\MappingOperation\Implementations\MapTo;
+use AutoMapperPlus\MappingOperation\MapperAwareOperation;
use function Functional\map;
/**
@@ -106,9 +106,7 @@ protected function doMap($source, $destination, MappingInterface $mapping)
foreach ($propertyNames as $propertyName) {
$mappingOperation = $mapping->getMappingOperationFor($propertyName);
- // @todo: find another solution to this hacky implementation of
- // recursive mapping.
- if ($mappingOperation instanceof MapTo) {
+ if ($mappingOperation instanceof MapperAwareOperation) {
$mappingOperation->setMapper($this);
}
diff --git a/src/MappingOperation/Implementations/FromProperty.php b/src/MappingOperation/Implementations/FromProperty.php
index f62a98b..f8a178a 100644
--- a/src/MappingOperation/Implementations/FromProperty.php
+++ b/src/MappingOperation/Implementations/FromProperty.php
@@ -2,11 +2,14 @@
namespace AutoMapperPlus\MappingOperation\Implementations;
+use AutoMapperPlus\AutoMapperInterface;
use AutoMapperPlus\Configuration\Options;
use AutoMapperPlus\MappingOperation\AlternativePropertyProvider;
use AutoMapperPlus\MappingOperation\DefaultMappingOperation;
+use AutoMapperPlus\MappingOperation\MapperAwareOperation;
use AutoMapperPlus\MappingOperation\MappingOperationInterface;
use AutoMapperPlus\MappingOperation\Reversible;
+use AutoMapperPlus\NameResolver\CallbackNameResolver;
/**
* Class FromProperty
@@ -15,8 +18,16 @@
*/
class FromProperty extends DefaultMappingOperation implements
AlternativePropertyProvider,
- Reversible
+ Reversible,
+ // We need to be mapper aware to be able to pass the mapper to a chained
+ // operation.
+ MapperAwareOperation
{
+ /**
+ * @var MappingOperationInterface|null
+ */
+ private $nextOperation;
+
/**
* @var string
*/
@@ -48,6 +59,22 @@ public function getAlternativePropertyName(): string
return $this->propertyName;
}
+ /**
+ * @inheritdoc
+ */
+ public function mapProperty(string $propertyName, $source, $destination): void {
+ if ($this->nextOperation === null) {
+ parent::mapProperty($propertyName, $source, $destination);
+ return;
+ }
+
+ $this->mapPropertyWithNextOperation(
+ $propertyName,
+ $source,
+ $destination
+ );
+ }
+
/**
* @inheritdoc
*/
@@ -71,4 +98,57 @@ public function getReverseTargetPropertyName
{
return $this->propertyName;
}
+
+ /**
+ * Chain a MapTo operation, making the MapTo use this operation's property
+ * name instead.
+ *
+ * Note: because MapTo is not reversible, the MapTo part gets lost when
+ * reversing the mapping.
+ *
+ * @todo: extend to other operations, or maybe a __call?
+ *
+ * @param string $class
+ * @return FromProperty
+ */
+ public function mapTo(string $class): FromProperty
+ {
+ $this->nextOperation = new MapTo($class);
+ return $this;
+ }
+
+ /**
+ * @param string $propertyName
+ * @param $source
+ * @param $destination
+ */
+ protected function mapPropertyWithNextOperation(
+ string $propertyName,
+ $source,
+ $destination
+ ): void
+ {
+ // We have to make the overridden property available to the next
+ // operation. To do this, we create a "one-time use" name resolver
+ // to pass to the operation.
+ $options = clone $this->options;
+ $options->setNameResolver(new CallbackNameResolver(function () {
+ return $this->propertyName;
+ }));
+ $this->nextOperation->setOptions($options);
+
+ // The chained operation will now use the property name assigned to
+ // FromProperty, so we can go ahead and call it.
+ $this->nextOperation->mapProperty($propertyName, $source, $destination);
+ }
+
+ /**
+ * @inheritdoc
+ */
+ public function setMapper(AutoMapperInterface $mapper): void
+ {
+ if ($this->nextOperation instanceof MapperAwareOperation) {
+ $this->nextOperation->setMapper($mapper);
+ }
+ }
}
diff --git a/src/MappingOperation/Implementations/MapFrom.php b/src/MappingOperation/Implementations/MapFrom.php
index a0ca27f..0b21461 100644
--- a/src/MappingOperation/Implementations/MapFrom.php
+++ b/src/MappingOperation/Implementations/MapFrom.php
@@ -14,7 +14,7 @@ class MapFrom extends DefaultMappingOperation
/**
* @var callable
*/
- private $valueCallback;
+ protected $valueCallback;
/**
* MapFrom constructor.
diff --git a/src/MappingOperation/Implementations/MapFromWithMapper.php b/src/MappingOperation/Implementations/MapFromWithMapper.php
new file mode 100644
index 0000000..9258a94
--- /dev/null
+++ b/src/MappingOperation/Implementations/MapFromWithMapper.php
@@ -0,0 +1,30 @@
+
+ * Date: 3/14/18
+ * Time: 1:16 PM
+ */
+
+namespace AutoMapperPlus\MappingOperation\Implementations;
+
+use AutoMapperPlus\MappingOperation\MapperAwareOperation;
+use AutoMapperPlus\MappingOperation\MapperAwareTrait;
+
+/**
+ * Class MapFromWithMapper
+ *
+ * @package AutoMapperPlus\MappingOperation\Implementations
+ */
+class MapFromWithMapper extends MapFrom implements MapperAwareOperation
+{
+ use MapperAwareTrait;
+
+ /**
+ * @inheritdoc
+ */
+ protected function getSourceValue($source, string $propertyName)
+ {
+ return ($this->valueCallback)($source, $this->mapper);
+ }
+}
diff --git a/src/MappingOperation/Implementations/MapTo.php b/src/MappingOperation/Implementations/MapTo.php
index 226b91d..b3831b7 100644
--- a/src/MappingOperation/Implementations/MapTo.php
+++ b/src/MappingOperation/Implementations/MapTo.php
@@ -2,8 +2,9 @@
namespace AutoMapperPlus\MappingOperation\Implementations;
-use AutoMapperPlus\AutoMapperInterface;
use AutoMapperPlus\MappingOperation\DefaultMappingOperation;
+use AutoMapperPlus\MappingOperation\MapperAwareOperation;
+use AutoMapperPlus\MappingOperation\MapperAwareTrait;
/**
* Class MapTo.
@@ -12,18 +13,15 @@
*
* @package AutoMapperPlus\MappingOperation\Implementations
*/
-class MapTo extends DefaultMappingOperation
+class MapTo extends DefaultMappingOperation implements MapperAwareOperation
{
+ use MapperAwareTrait;
+
/**
* @var string
*/
private $destinationClass;
- /**
- * @var AutoMapperInterface
- */
- private $mapper;
-
/**
* MapTo constructor.
*
@@ -42,20 +40,15 @@ public function getDestinationClass(): string
return $this->destinationClass;
}
- /**
- * @param AutoMapperInterface $mapper
- */
- public function setMapper(AutoMapperInterface $mapper)
- {
- $this->mapper = $mapper;
- }
-
/**
* @inheritdoc
*/
protected function getSourceValue($source, string $propertyName)
{
- $value = $this->getPropertyAccessor()->getProperty($source, $propertyName);
+ $value = $this->getPropertyAccessor()->getProperty(
+ $source,
+ $this->getSourcePropertyName($propertyName)
+ );
return $this->isCollection($value)
? $this->mapper->mapMultiple($value, $this->destinationClass)
diff --git a/src/MappingOperation/MapperAwareOperation.php b/src/MappingOperation/MapperAwareOperation.php
new file mode 100644
index 0000000..8242c2f
--- /dev/null
+++ b/src/MappingOperation/MapperAwareOperation.php
@@ -0,0 +1,17 @@
+mapper = $mapper;
+ }
+}
diff --git a/src/MappingOperation/Operation.php b/src/MappingOperation/Operation.php
index 540ae77..ddeabb7 100644
--- a/src/MappingOperation/Operation.php
+++ b/src/MappingOperation/Operation.php
@@ -5,6 +5,7 @@
use AutoMapperPlus\MappingOperation\Implementations\FromProperty;
use AutoMapperPlus\MappingOperation\Implementations\Ignore;
use AutoMapperPlus\MappingOperation\Implementations\MapFrom;
+use AutoMapperPlus\MappingOperation\Implementations\MapFromWithMapper;
use AutoMapperPlus\MappingOperation\Implementations\MapTo;
/**
@@ -28,6 +29,22 @@ public static function mapFrom(callable $valueCallback): MapFrom
return new MapFrom($valueCallback);
}
+ /**
+ * Set a property's value from callback, callback should contain 2 parameters
+ *
+ * @param callable $valueCallback
+ * Callback definition:
+ *
+ * function(AutoMapperInterface, mixed){
+
+ * }
+ * @return MapFromWithMapper
+ */
+ public static function mapFromWithMapper(callable $valueCallback): MapFromWithMapper
+ {
+ return new MapFromWithMapper($valueCallback);
+ }
+
/**
* Ignore a property.
*
diff --git a/test/AutoMapperTest.php b/test/AutoMapperTest.php
index 90c0f58..0d76528 100644
--- a/test/AutoMapperTest.php
+++ b/test/AutoMapperTest.php
@@ -10,6 +10,10 @@
use AutoMapperPlus\NameConverter\NamingConvention\SnakeCaseNamingConvention;
use AutoMapperPlus\NameResolver\CallbackNameResolver;
use AutoMapperPlus\Test\Models\Inheritance\SourceChild;
+use AutoMapperPlus\Test\Models\Nested\Address;
+use AutoMapperPlus\Test\Models\Nested\AddressDto;
+use AutoMapperPlus\Test\Models\Nested\Person;
+use AutoMapperPlus\Test\Models\Nested\PersonDto;
use AutoMapperPlus\Test\Models\SimpleProperties\NoProperties;
use PHPUnit\Framework\TestCase;
use AutoMapperPlus\Test\Models\Employee\Employee;
@@ -456,4 +460,79 @@ public function testANullObjectReturnsNull()
$result = $mapper->map($source, Destination::class);
$this->assertEquals(null, $result);
}
+
+ public function testInvalidWithMappingCallback_ThrowsException()
+ {
+ // Arrange
+ $source = new CamelCaseSource();
+ $error = null;
+
+ // Act
+ $this->config->registerMapping(CamelCaseSource::class, \stdClass::class)
+ ->forMember('propertyName', Operation::mapFromWithMapper(function($source, AutoMapperInterface $mapping){
+ return 13;
+ }));
+ $mapper = new AutoMapper($this->config);
+ $result = $mapper->map($source, \stdClass::class);
+
+ // Assert
+ $this->assertEquals($result, $result);
+ }
+
+ public function testInstanceWithMappingCallback_InstanceIsCorrect()
+ {
+ // Arrange
+ $propertyStdClass = new \stdClass();
+ $propertyStdClass->value = "TestValue";
+
+ $testSuffix = "MAPPED";
+ $expectedResult = $propertyStdClass->value . $testSuffix;
+
+ $this->config->registerMapping(\stdClass::class, Destination::class)
+ ->forMember('name', function($source) use ($testSuffix){
+ return $source->value . $testSuffix;
+ });
+
+ $this->config->registerMapping(CamelCaseSource::class, \stdClass::class)
+ ->forMember('propertyName', Operation::mapFromWithMapper(function($source, AutoMapperInterface $mapping){
+ // if the $mapping isn't a instance of AutoMapperInterface, it wouldn't return anything
+
+ return $mapping->map($source->propertyName, Destination::class);
+ }));
+ $mapper = new AutoMapper($this->config);
+ $source = new CamelCaseSource();
+ $source->propertyName = $propertyStdClass;
+
+ // Act
+ $result = $mapper->map($source, \stdClass::class);
+
+ // Assert
+ $this->assertEquals($expectedResult, $result->propertyName->name);
+ }
+
+ /**
+ * @todo: move this to fromPropertyTest.
+ */
+ public function testFromPropertyCanBeChained()
+ {
+ $config = new AutoMapperConfig();
+ $config->registerMapping(Address::class, AddressDto::class)
+ ->forMember('streetAndNumber', function (Address $source) {
+ return $source->street . ' ' . $source->number;
+ });
+ $config->registerMapping(Person::class, PersonDto::class)
+ ->forMember('address', Operation::fromProperty('adres')->mapTo(AddressDto::class))
+ ;
+ $mapper = new AutoMapper($config);
+
+ $address = new Address;
+ $address->street = "Main Street";
+ $address->number = 12;
+ $person = new Person;
+ $person->adres = $address;
+
+ $result = $mapper->map($person, PersonDto::class);
+
+ $this->assertEquals("Main Street 12", $result->address->streetAndNumber);
+ }
}
diff --git a/test/MappingOperation/Implementations/MapFromWithMappingTest.php b/test/MappingOperation/Implementations/MapFromWithMappingTest.php
new file mode 100644
index 0000000..798efb1
--- /dev/null
+++ b/test/MappingOperation/Implementations/MapFromWithMappingTest.php
@@ -0,0 +1,62 @@
+setMapper(AutoMapper::initialize(function (AutoMapperConfigInterface $config) {
+ $config->registerMapping(Source::class, Destination::class);
+ }));
+ $mapFromWithMapper->setOptions(Options::default());
+
+ $source = new Source();
+ $destination = new Destination();
+
+ // Act
+ $mapFromWithMapper->mapProperty('name', $source, $destination);
+
+ // Assert
+ $this->assertEquals(42, $destination->name);
+ }
+
+ public function testItReceivesTheSourceObject()
+ {
+ // Arrange
+ $mapFromWithMapper = new MapFromWithMapper(function ($source, AutoMapperInterface $mapper) {
+ return $source;
+ });
+ $mapFromWithMapper->setMapper(AutoMapper::initialize(function (AutoMapperConfigInterface $config) {
+ $config->registerMapping(Source::class, Destination::class);
+ }));
+ $mapFromWithMapper->setOptions(Options::default());
+
+ $source = new Source();
+ $destination = new Destination();
+
+ // Act
+ $mapFromWithMapper->mapProperty('name', $source, $destination);
+
+ // Assert
+ $this->assertInstanceOf(Source::class, $destination->name);
+ }
+}
diff --git a/test/MappingOperation/Implementations/MapToTest.php b/test/MappingOperation/Implementations/MapToTest.php
index d07c5ea..24e9f11 100644
--- a/test/MappingOperation/Implementations/MapToTest.php
+++ b/test/MappingOperation/Implementations/MapToTest.php
@@ -5,8 +5,8 @@
use AutoMapperPlus\AutoMapper;
use AutoMapperPlus\Configuration\AutoMapperConfigInterface;
use AutoMapperPlus\Configuration\Options;
+use AutoMapperPlus\NameResolver\CallbackNameResolver;
use PHPUnit\Framework\TestCase;
-use AutoMapperPlus\Test\Models\Nested\ChildClass;
use AutoMapperPlus\Test\Models\Nested\ParentClass;
use AutoMapperPlus\Test\Models\Nested\ParentClassDto;
use AutoMapperPlus\Test\Models\SimpleProperties\Destination;
@@ -69,4 +69,33 @@ public function testItCanMapMultiple()
$this->assertEquals('SourceName1', $parentDestination->child[0]->name);
$this->assertInstanceOf(Destination::class, $parentDestination->child[1]);
}
+
+ /**
+ * Ensure the operation uses the assigned name resolver. See #17.
+ */
+ public function testItUsesTheNameResolver()
+ {
+ $mapTo = new MapTo(Destination::class);
+ $options = Options::default();
+ // Set a name resolver to always use the property 'child' of the source.
+ $options->setNameResolver(new CallbackNameResolver(function () {
+ return 'child';
+ }));
+ $mapTo->setOptions($options);
+ $mapTo->setMapper(AutoMapper::initialize(function (AutoMapperConfigInterface $config) {
+ $config->registerMapping(Source::class, Destination::class);
+ }));
+
+ $parent = new ParentClass();
+ $child = new Source('SourceName');
+ $parent->child = $child;
+ $parentDestination = new ParentClassDto();
+
+ $mapTo->mapProperty('anotherProperty', $parent, $parentDestination);
+
+ // Because of the name resolver, we expect the value to be set
+ // correctly.
+ $this->assertInstanceOf(Destination::class, $parentDestination->anotherProperty);
+ $this->assertEquals('SourceName', $parentDestination->anotherProperty->name);
+ }
}
diff --git a/test/Models/Nested/Address.php b/test/Models/Nested/Address.php
new file mode 100644
index 0000000..722cc20
--- /dev/null
+++ b/test/Models/Nested/Address.php
@@ -0,0 +1,9 @@
+