From fcf58e3069f338230a0d1264f4222e698bf752c5 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 28 Jan 2021 13:49:33 +0100 Subject: [PATCH 1/2] Add fromMethod operation --- README.md | 11 +- src/Configuration/Options.php | 23 ++++ .../DefaultMappingOperation.php | 15 +++ .../Implementations/FromMethod.php | 125 ++++++++++++++++++ .../MethodReaderInterface.php | 25 ++++ src/PropertyAccessor/PropertyAccessor.php | 18 ++- .../PropertyAccessorInterface.php | 2 +- .../Implementations/FromMethodTest.php | 28 ++++ .../Implementations/FromPropertyTest.php | 13 ++ test/Models/Visibility/Visibility.php | 5 + 10 files changed, 262 insertions(+), 3 deletions(-) create mode 100644 src/MappingOperation/Implementations/FromMethod.php create mode 100644 src/PropertyAccessor/MethodReaderInterface.php create mode 100644 test/MappingOperation/Implementations/FromMethodTest.php diff --git a/README.md b/README.md index 15b4f7c..19b41d7 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,11 @@ class Employee { return $this->id; } + + public function getFullName() + { + return $this->firstName .' '. $this->lastName; + } // And so on... } @@ -92,6 +97,7 @@ class EmployeeDto public $firstName; public $lastName; public $age; + public $fullName; } ``` @@ -219,6 +225,7 @@ The following operations are provided: | Ignore | Ignores the property. | | 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. | +| FromMethod | Use this to map your value from a **public** method. This Operation does not support reverse mapping | | DefaultMappingOperation | Simply transfers the property, taking into account the provided naming conventions (if there are any). | | SetTo | Always sets the property to the given value | @@ -243,7 +250,9 @@ $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 +// Map property from given method name of the source object. +$mapping->forMember('fullName', Operation::fromMethod('getFullName')); +// The `FromProperty` and `FromMethod` operations can be chained with `MapTo`, allowing a // differently named property to be mapped to a class. $mapping->forMember( 'address', diff --git a/src/Configuration/Options.php b/src/Configuration/Options.php index 5707201..0e45bb3 100644 --- a/src/Configuration/Options.php +++ b/src/Configuration/Options.php @@ -8,6 +8,7 @@ use AutoMapperPlus\NameConverter\NamingConvention\NamingConventionInterface; use AutoMapperPlus\NameResolver\NameResolver; use AutoMapperPlus\NameResolver\NameResolverInterface; +use AutoMapperPlus\PropertyAccessor\MethodReaderInterface; use AutoMapperPlus\PropertyAccessor\PropertyAccessor; use AutoMapperPlus\PropertyAccessor\PropertyAccessorInterface; use AutoMapperPlus\PropertyAccessor\PropertyReaderInterface; @@ -50,6 +51,11 @@ class Options */ private $propertyReader; + /** + * @var MethodReaderInterface + */ + private $methodReader; + /** * @var NameResolverInterface */ @@ -177,6 +183,7 @@ public function setPropertyAccessor(PropertyAccessorInterface $propertyAccessor) $this->propertyReader = $propertyAccessor; $this->propertyWriter = $propertyAccessor; $this->propertyAccessor = $propertyAccessor; + $this->methodReader = $propertyAccessor; } /** @@ -211,6 +218,22 @@ public function setPropertyReader(PropertyReaderInterface $propertyReader): void $this->propertyReader = $propertyReader; } + /** + * @return MethodReaderInterface + */ + public function getMethodReader(): MethodReaderInterface + { + return $this->methodReader ?: $this->propertyAccessor; + } + + /** + * @param MethodReaderInterface $methodReader + */ + public function setMethodReader(MethodReaderInterface $methodReader): void + { + $this->methodReader = $methodReader; + } + /** * @return MappingOperationInterface */ diff --git a/src/MappingOperation/DefaultMappingOperation.php b/src/MappingOperation/DefaultMappingOperation.php index 65eb568..4dca28c 100644 --- a/src/MappingOperation/DefaultMappingOperation.php +++ b/src/MappingOperation/DefaultMappingOperation.php @@ -4,6 +4,7 @@ use AutoMapperPlus\Configuration\Options; use AutoMapperPlus\NameResolver\NameResolverInterface; +use AutoMapperPlus\PropertyAccessor\MethodReaderInterface; use AutoMapperPlus\PropertyAccessor\PropertyReaderInterface; use AutoMapperPlus\PropertyAccessor\PropertyWriterInterface; @@ -34,6 +35,11 @@ class DefaultMappingOperation implements MappingOperationInterface */ protected $propertyReader; + /** + * @var MethodReaderInterface + */ + protected $methodReader; + /** * @var PropertyWriterInterface */ @@ -60,6 +66,7 @@ public function setOptions(Options $options): void $this->options = $options; $this->nameResolver = $options->getNameResolver(); $this->propertyReader = $options->getPropertyReader(); + $this->methodReader = $options->getMethodReader(); $this->propertyWriter = $options->getPropertyWriter(); } @@ -126,6 +133,14 @@ protected function getPropertyReader(): PropertyReaderInterface return $this->propertyReader; } + /** + * @return MethodReaderInterface + */ + protected function getMethodReader(): MethodReaderInterface + { + return $this->methodReader; + } + /** * @return PropertyWriterInterface */ diff --git a/src/MappingOperation/Implementations/FromMethod.php b/src/MappingOperation/Implementations/FromMethod.php new file mode 100644 index 0000000..f665f68 --- /dev/null +++ b/src/MappingOperation/Implementations/FromMethod.php @@ -0,0 +1,125 @@ +methodName = $methodName; + } + + /** + * @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 + */ + protected function canMapProperty(string $propertyName, $source): bool + { + $sourcePropertyName = $this->getSourcePropertyName($propertyName); + + return $this->getMethodReader()->hasMethod($source, $sourcePropertyName); + } + + /** + * @inheritdoc + */ + protected function getSourceValue($source, string $propertyName) + { + return $this->getMethodReader()->getMethod( + $source, + $this->getSourcePropertyName($propertyName) + ); + } + + /** + * @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->methodName; + })); + $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 getSourcePropertyName(string $propertyName): string + { + return $this->methodName; + } + + /** + * @inheritdoc + */ + public function getAlternativePropertyName(): string + { + return $this->methodName; + } + + /** + * @inheritdoc + */ + public function setMapper(AutoMapperInterface $mapper): void + { + if ($this->nextOperation instanceof MapperAwareOperation) { + $this->nextOperation->setMapper($mapper); + } + } +} diff --git a/src/PropertyAccessor/MethodReaderInterface.php b/src/PropertyAccessor/MethodReaderInterface.php new file mode 100644 index 0000000..7c8730b --- /dev/null +++ b/src/PropertyAccessor/MethodReaderInterface.php @@ -0,0 +1,25 @@ +isMethodPublic($object, $methodName); + } + + public function getMethod($object, string $methodName) + { + return $object->{$methodName}(); + } + + protected function isMethodPublic($object, string $methodName): bool + { + return (new ReflectionMethod($object, $methodName))->isPublic(); + } } diff --git a/src/PropertyAccessor/PropertyAccessorInterface.php b/src/PropertyAccessor/PropertyAccessorInterface.php index 45cf9d9..038e9fd 100644 --- a/src/PropertyAccessor/PropertyAccessorInterface.php +++ b/src/PropertyAccessor/PropertyAccessorInterface.php @@ -7,7 +7,7 @@ * * @package AutoMapperPlus\PropertyAccessor */ -interface PropertyAccessorInterface extends PropertyWriterInterface, PropertyReaderInterface +interface PropertyAccessorInterface extends PropertyWriterInterface, PropertyReaderInterface, MethodReaderInterface { // } diff --git a/test/MappingOperation/Implementations/FromMethodTest.php b/test/MappingOperation/Implementations/FromMethodTest.php new file mode 100644 index 0000000..ee2926a --- /dev/null +++ b/test/MappingOperation/Implementations/FromMethodTest.php @@ -0,0 +1,28 @@ +setOptions(Options::default()); + + $source = new Visibility(); + $destination = new Destination(); + + $operation->mapProperty('name', $source, $destination); + + $this->assertSame('foo', $destination->name); + } +} diff --git a/test/MappingOperation/Implementations/FromPropertyTest.php b/test/MappingOperation/Implementations/FromPropertyTest.php index 49673f6..df5a540 100644 --- a/test/MappingOperation/Implementations/FromPropertyTest.php +++ b/test/MappingOperation/Implementations/FromPropertyTest.php @@ -26,4 +26,17 @@ public function testItMapsAProperty() $this->assertTrue($destination->name); } + + public function testItMapsAMethod() + { + $operation = new FromProperty('getMethodValue'); + $operation->setOptions(Options::default()); + + $source = new Visibility(); + $destination = new Destination(); + + $operation->mapProperty('name', $source, $destination); + + $this->assertEmpty($destination->name); + } } diff --git a/test/Models/Visibility/Visibility.php b/test/Models/Visibility/Visibility.php index b22b0d4..5328dd4 100644 --- a/test/Models/Visibility/Visibility.php +++ b/test/Models/Visibility/Visibility.php @@ -28,4 +28,9 @@ public function getPrivateProperty() return $this->privateProperty; } + public function getMethodValue() + { + return 'foo'; + } + } From b08eb1532cdeef278df19df1f2d03cd0d9d093d6 Mon Sep 17 00:00:00 2001 From: Peter Date: Thu, 28 Jan 2021 13:55:51 +0100 Subject: [PATCH 2/2] Add FromMethod mapping operation --- test/Configuration/OptionsTest.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/Configuration/OptionsTest.php b/test/Configuration/OptionsTest.php index 60e7ac9..5f7576b 100644 --- a/test/Configuration/OptionsTest.php +++ b/test/Configuration/OptionsTest.php @@ -2,6 +2,7 @@ namespace AutoMapperPlus\Configuration; +use AutoMapperPlus\PropertyAccessor\MethodReaderInterface; use AutoMapperPlus\PropertyAccessor\PropertyAccessorInterface; use AutoMapperPlus\PropertyAccessor\PropertyReaderInterface; use AutoMapperPlus\PropertyAccessor\PropertyWriterInterface; @@ -48,6 +49,18 @@ public function testPropertyReaderCanBeOverridden() $this->assertEquals($reader, $options->getPropertyReader()); } + public function testMethodReaderCanBeOverridden() + { + $options = new Options(); + $accessor = $this->createMock(PropertyAccessorInterface::class); + $reader = $this->createMock(MethodReaderInterface::class); + $options->setPropertyAccessor($accessor); + $options->setMethodReader($reader); + + $this->assertEquals($accessor, $options->getPropertyAccessor()); + $this->assertEquals($reader, $options->getMethodReader()); + } + public function testPropertyWriterDefaultsToAccessor() { $options = new Options(); @@ -65,4 +78,13 @@ public function testPropertyReaderDefaultsToAccessor() $this->assertEquals($accessor, $options->getPropertyReader()); } + + public function testMethodReaderDefaultsToAccessor() + { + $options = new Options(); + $accessor = $this->createMock(PropertyAccessorInterface::class); + $options->setPropertyAccessor($accessor); + + $this->assertEquals($accessor, $options->getMethodReader()); + } }