diff --git a/README.md b/README.md index 15b4f7c..de3d148 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ Transfers data from one object to another, allowing custom mapping operations. * [The concept of object crates](#the-concept-of-object-crates) * [Mapping with arrays](#mapping-with-arrays) * [Using a custom mapper](#using-a-custom-mapper) + * [Using middlewares](#using-middlewares) * [Adding context](#adding-context) * [Misc](#misc) * [Similar libraries](#similar-libraries) @@ -805,6 +806,46 @@ $employee = new Employee(10, 'John', 'Doe', 1980); $result = $mapper->map($employee, EmployeeDto::class); ``` +### Using middlewares +You can register middlewares to customize how automapper works internally and define +global behaviors. + +The following example will set 42 to any `id` property that would have been `null`. + +```php +getOptions()->getPropertyReader()->getProperty($destination, $propertyName); + if ($defaultValue === NULL) { + $mapping->getOptions()->getPropertyWriter()->setProperty($destination, $propertyName, 42); + } + } + $next(); + } +} + +$config->registerMiddlewares(new AnwserToUniverseMiddleware()); +$config->registerMapping(Employee::class, EmployeeDto::class); +$mapper = new AutoMapper($config); + +// The AutoMapper can now be used as usual, but your middleware will intercept some property mappings. +$employee = new Employee(NULL, 'John', 'Doe', 1980); +$result = $mapper->map($employee, EmployeeDto::class); +echo $result->id; // => 42 +``` + ### Adding context Sometimes a mapping should behave differently based on the context. It is therefore possible to pass a third argument to the map methods to describe @@ -936,7 +977,7 @@ Please note that this is a temporary solution. The issue will be fixed in the - [ ] Allow setting a maximum depth, see #14 - [ ] Provide a NameResolver that accepts an array mapping, as an alternative to multiple `FromProperty`s - [ ] Make use of a decorated Symfony's `PropertyAccessor` (see [#16](https://github.com/mark-gerarts/automapper-plus/issues/16)) -- [ ] Allow adding of middleware to the mapper +- [x] Allow adding of middleware to the mapper - [ ] Allow mapping *to* array *[Version 2](https://github.com/mark-gerarts/automapper-plus/tree/2.0) is in the works, check there for new features as well* diff --git a/src/AutoMapper.php b/src/AutoMapper.php index cbbfdfe..5ea2950 100644 --- a/src/AutoMapper.php +++ b/src/AutoMapper.php @@ -9,8 +9,8 @@ use AutoMapperPlus\Exception\InvalidArgumentException; use AutoMapperPlus\Exception\UnregisteredMappingException; use AutoMapperPlus\Exception\UnsupportedSourceTypeException; -use AutoMapperPlus\MappingOperation\ContextAwareOperation; use AutoMapperPlus\MappingOperation\MapperAwareOperation; +use AutoMapperPlus\Middleware\MapperMiddleware; /** * Class AutoMapper @@ -188,7 +188,7 @@ public function mapToObject($source, $destination, array $context = []) } /** - * Performs the actual transferring of properties. + * Performs the actual transferring of properties, involving all matching mapper and property middleware. * * @param $source * @param $destination @@ -204,29 +204,24 @@ protected function doMap( array $context = [] ) { - $propertyNames = $mapping->getTargetProperties($destination, $source); - foreach ($propertyNames as $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); - } - - $mappingOperation->mapProperty( - $propertyName, - $source, - $destination - ); - } finally { - $this->pop(self::PROPERTY_STACK_CONTEXT, $context); - } + $mapper = $this; + + $this->autoMapperConfig->getDefaultMapperMiddleware()->map($source, $destination, $mapper, $mapping, $context, function () { + }); + + $map = function () { + // NOOP + }; + + foreach (array_reverse($this->getMapperMiddlewares()) as $middleware) { + $map = function () use ($middleware, $source, $destination, $mapper, $mapping, $context, $map) { + /** @var MapperMiddleware $middleware */ + return $middleware->map($source, $destination, $mapper, $mapping, $context, $map); + }; } + $map(); + return $destination; } @@ -279,4 +274,11 @@ private function getCustomMapper(MappingInterface $mapping): ?MapperInterface return $customMapper; } + + private function getMapperMiddlewares() + { + return array_filter($this->getConfiguration()->getMapperMiddlewares(), function ($middleware) { + return $middleware !== $this->getConfiguration()->getDefaultMapperMiddleware(); + }); + } } diff --git a/src/Configuration/AutoMapperConfig.php b/src/Configuration/AutoMapperConfig.php index 9efd713..8e05f9b 100644 --- a/src/Configuration/AutoMapperConfig.php +++ b/src/Configuration/AutoMapperConfig.php @@ -2,6 +2,13 @@ namespace AutoMapperPlus\Configuration; +use AutoMapperPlus\Middleware\DefaultMapperMiddleware; +use AutoMapperPlus\Middleware\DefaultMiddleware; +use AutoMapperPlus\Middleware\DefaultPropertyMiddleware; +use AutoMapperPlus\Middleware\MapperMiddleware; +use AutoMapperPlus\Middleware\Middleware; +use AutoMapperPlus\Middleware\PropertyMiddleware; + /** * Class AutoMapperConfig * @@ -14,6 +21,22 @@ class AutoMapperConfig implements AutoMapperConfigInterface */ private $mappings = []; + /** @var MapperMiddleware */ + private $defaultMapperMiddleware; + + /** @var PropertyMiddleware */ + private $defaultPropertyMiddleware; + + /** + * @var MapperMiddleware[] + */ + private $mapperMiddlewares = []; + + /** + * @var PropertyMiddleware[] + */ + private $propertyMiddlewares = []; + /** * @var Options */ @@ -30,6 +53,8 @@ public function __construct(callable $configurator = null) if ($configurator !== null) { $configurator($this->options); } + $this->defaultMapperMiddleware = new DefaultMapperMiddleware(); + $this->defaultPropertyMiddleware = new DefaultPropertyMiddleware(); } /** @@ -202,6 +227,27 @@ public function registerMapping( return $mapping; } + + public function registerMiddlewares(Middleware ...$middlewares): AutoMapperConfigInterface + { + foreach ($middlewares as $middleware) { + if ($middleware instanceof MapperMiddleware) { + $this->mapperMiddlewares[] = $middleware; + if ($middleware instanceof DefaultMiddleware) { + $this->defaultMapperMiddleware = $middleware; + } + } + if ($middleware instanceof PropertyMiddleware) { + $this->propertyMiddlewares[] = $middleware; + if ($middleware instanceof DefaultMiddleware) { + $this->defaultPropertyMiddleware = $middleware; + } + } + } + + return $this; + } + /** * @inheritdoc */ @@ -209,4 +255,36 @@ public function getOptions(): Options { return $this->options; } + + /** + * @return PropertyMiddleware + */ + public function getDefaultPropertyMiddleware(): PropertyMiddleware + { + return $this->defaultPropertyMiddleware; + } + + /** + * @return MapperMiddleware + */ + public function getDefaultMapperMiddleware(): MapperMiddleware + { + return $this->defaultMapperMiddleware; + } + + /** + * @inheritdoc + */ + public function getMapperMiddlewares() + { + return $this->mapperMiddlewares; + } + + /** + * @inheritdoc + */ + public function getPropertyMiddlewares() + { + return $this->propertyMiddlewares; + } } diff --git a/src/Configuration/AutoMapperConfigInterface.php b/src/Configuration/AutoMapperConfigInterface.php index 7eb6d07..8dd0cb4 100644 --- a/src/Configuration/AutoMapperConfigInterface.php +++ b/src/Configuration/AutoMapperConfigInterface.php @@ -2,6 +2,11 @@ namespace AutoMapperPlus\Configuration; +use AutoMapperPlus\Middleware\DefaultMiddleware; +use AutoMapperPlus\Middleware\MapperMiddleware; +use AutoMapperPlus\Middleware\Middleware; +use AutoMapperPlus\Middleware\PropertyMiddleware; + /** * Interface AutoMapperConfigInterface * @@ -47,8 +52,42 @@ public function registerMapping( string $destinationClassName ): MappingInterface; + /** + * Register middlewares after existing ones. + * + * All middlewares will be invoked in order. + * + * @param Middleware ...$middlewares + * @return self + * + * @see DefaultMiddleware + * @see PropertyMiddleware + * @see MapperMiddleware + */ + public function registerMiddlewares(Middleware ...$middlewares): AutoMapperConfigInterface; + /** * @return Options */ public function getOptions(): Options; + + /** + * @return MapperMiddleware + */ + public function getDefaultMapperMiddleware(); + + /** + * @return PropertyMiddleware + */ + public function getDefaultPropertyMiddleware(); + + /** + * @return MapperMiddleware[] + */ + public function getMapperMiddlewares(); + + /** + * @return PropertyMiddleware[] + */ + public function getPropertyMiddlewares(); } diff --git a/src/Middleware/DefaultMapperMiddleware.php b/src/Middleware/DefaultMapperMiddleware.php new file mode 100644 index 0000000..a71c2cb --- /dev/null +++ b/src/Middleware/DefaultMapperMiddleware.php @@ -0,0 +1,88 @@ +getTargetProperties($destination, $source); + foreach ($propertyNames as $propertyName) { + $this->push(AutoMapper::PROPERTY_STACK_CONTEXT, $propertyName, $context); + try { + $operation = $mapping->getMappingOperationFor($propertyName); + + if ($operation instanceof MapperAwareOperation) { + $operation->setMapper($mapper); + } + if ($operation instanceof ContextAwareOperation) { + $operation->setContext($context); + } + + $mapper->getConfiguration()->getDefaultPropertyMiddleware()->mapProperty( + $propertyName, + $source, + $destination, + $mapper, + $mapping, + $operation, + $context, function () { + // NOOP + }); + + $mapProperty = function () { + // NOOP + }; + + foreach (array_reverse($this->getPropertyMiddleware($mapper->getConfiguration())) as $middleware) { + $mapProperty = function () use ($middleware, $propertyName, $source, $destination, $mapper, $mapping, $operation, $context, $mapProperty) { + /** @var PropertyMiddleware $middleware */ + return $middleware->mapProperty($propertyName, $source, $destination, $mapper, $mapping, $operation, $context, $mapProperty); + }; + } + + $mapProperty(); + } finally { + $this->pop(AutoMapper::PROPERTY_STACK_CONTEXT, $context); + } + } + } + + public function map($source, $destination, AutoMapperInterface $mapper, MappingInterface $mapping, array $context, callable $next) + { + $this->doMap($source, $destination, $mapper, $mapping, $context); + $next(); + } + + 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]); + } + + private function getPropertyMiddleware(AutoMapperConfigInterface $configuration) + { + return array_filter($configuration->getPropertyMiddlewares(), function ($middleware) use ($configuration) { + return $middleware !== $configuration->getDefaultPropertyMiddleware(); + }); + } +} \ No newline at end of file diff --git a/src/Middleware/DefaultMiddleware.php b/src/Middleware/DefaultMiddleware.php new file mode 100644 index 0000000..2ddab89 --- /dev/null +++ b/src/Middleware/DefaultMiddleware.php @@ -0,0 +1,14 @@ +mapProperty( + $propertyName, + $source, + $destination + ); + } + + public function mapProperty($propertyName, + $source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + MappingOperationInterface $operation, + array $context, + callable $next) + { + $this->doMapProperty($propertyName, $source, $destination, $mapper, $mapping, $operation, $context); + $next(); + } +} \ No newline at end of file diff --git a/src/Middleware/MapperMiddleware.php b/src/Middleware/MapperMiddleware.php new file mode 100644 index 0000000..fb443c2 --- /dev/null +++ b/src/Middleware/MapperMiddleware.php @@ -0,0 +1,34 @@ +getOptions()->getPropertyReader()->getProperty($destination, $propertyName); + if ($defaultValue === NULL) { + $mapping->getOptions()->getPropertyWriter()->setProperty($destination, $propertyName, 42); + } + } + $next(); + } +} \ No newline at end of file diff --git a/test/Middleware/AppendMapperMiddleware.php b/test/Middleware/AppendMapperMiddleware.php new file mode 100644 index 0000000..fe8f0e3 --- /dev/null +++ b/test/Middleware/AppendMapperMiddleware.php @@ -0,0 +1,38 @@ +value = $value; + $this->propertyNames = $propertyNames; + } + + public function map($source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + array $context, + callable $next) + { + foreach ($this->propertyNames as $propertyName) { + $destination->{$propertyName} = $destination->{$propertyName} . $this->value; + } + $next(); + } +} \ No newline at end of file diff --git a/test/Middleware/AppendPropertyMiddleware.php b/test/Middleware/AppendPropertyMiddleware.php new file mode 100644 index 0000000..a7190bc --- /dev/null +++ b/test/Middleware/AppendPropertyMiddleware.php @@ -0,0 +1,41 @@ +value = $value; + $this->propertyNames = $propertyNames; + } + + public function mapProperty($propertyName, + $source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + MappingOperationInterface $operation, + array $context, + callable $next) + { + if (in_array($propertyName, $this->propertyNames)) { + $destination->{$propertyName} = $destination->{$propertyName} . $this->value; + } + $next(); + } +} \ No newline at end of file diff --git a/test/Middleware/PrependMapperMiddleware.php b/test/Middleware/PrependMapperMiddleware.php new file mode 100644 index 0000000..5d6aa83 --- /dev/null +++ b/test/Middleware/PrependMapperMiddleware.php @@ -0,0 +1,38 @@ +value = $value; + $this->propertyNames = $propertyNames; + } + + public function map($source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + array $context, + callable $next) + { + foreach ($this->propertyNames as $propertyName) { + $destination->{$propertyName} = $this->value . $destination->{$propertyName}; + } + $next(); + } +} \ No newline at end of file diff --git a/test/Middleware/PrependPropertyMiddleware.php b/test/Middleware/PrependPropertyMiddleware.php new file mode 100644 index 0000000..2b140b8 --- /dev/null +++ b/test/Middleware/PrependPropertyMiddleware.php @@ -0,0 +1,41 @@ +value = $value; + $this->propertyNames = $propertyNames; + } + + public function mapProperty($propertyName, + $source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + MappingOperationInterface $operation, + array $context, + callable $next) + { + if (in_array($propertyName, $this->propertyNames)) { + $destination->{$propertyName} = $this->value . $destination->{$propertyName} ; + } + $next(); + } +} \ No newline at end of file diff --git a/test/Middleware/ValueMapperDefaultMiddleware.php b/test/Middleware/ValueMapperDefaultMiddleware.php new file mode 100644 index 0000000..48e93e5 --- /dev/null +++ b/test/Middleware/ValueMapperDefaultMiddleware.php @@ -0,0 +1,12 @@ +value = $value; + $this->propertyNames = $propertyNames; + } + + public function map($source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + array $context, + callable $next) + { + foreach ($this->propertyNames as $propertyName) { + $destination->{$propertyName} = $this->value; + } + $next(); + } +} \ No newline at end of file diff --git a/test/Middleware/ValuePropertyDefaultMiddleware.php b/test/Middleware/ValuePropertyDefaultMiddleware.php new file mode 100644 index 0000000..fb878a6 --- /dev/null +++ b/test/Middleware/ValuePropertyDefaultMiddleware.php @@ -0,0 +1,12 @@ +value = $value; + $this->propertyNames = $propertyNames; + } + + public function mapProperty($propertyName, + $source, + $destination, + AutoMapperInterface $mapper, + MappingInterface $mapping, + MappingOperationInterface $operation, + array $context, + callable $next) + { + if (in_array($propertyName, $this->propertyNames)) { + $destination->{$propertyName} = $this->value; + } + $next(); + } +} \ No newline at end of file diff --git a/test/Models/Employee/Employee.php b/test/Models/Employee/Employee.php index 5ca526f..1edb393 100644 --- a/test/Models/Employee/Employee.php +++ b/test/Models/Employee/Employee.php @@ -15,7 +15,7 @@ class Employee private $birthYear; private $address; - function __construct(int $id, string $firstName, string $lastName, int $birthYear, $address = null) + function __construct(?int $id, string $firstName, string $lastName, int $birthYear, $address = null) { $this->id = $id; $this->firstName = $firstName; diff --git a/test/Models/SimpleProperties/CompleteSource.php b/test/Models/SimpleProperties/CompleteSource.php new file mode 100644 index 0000000..9b1a916 --- /dev/null +++ b/test/Models/SimpleProperties/CompleteSource.php @@ -0,0 +1,21 @@ +name = $name; + $this->anotherProperty = $anotherProperty; + } +} diff --git a/test/Scenarios/MiddlewareTest.php b/test/Scenarios/MiddlewareTest.php new file mode 100644 index 0000000..a95ec22 --- /dev/null +++ b/test/Scenarios/MiddlewareTest.php @@ -0,0 +1,147 @@ +registerMiddlewares(new ValueMapperMiddleware('mapper middleware value', 'name')); + $config->registerMapping(CompleteSource::class, Destination::class); + $mapper = new AutoMapper($config); + $source = new CompleteSource('a name', 'another property'); + + /** @var Destination $result */ + $result = $mapper->map($source, Destination::class); + + $this->assertEquals('mapper middleware value', $result->name); + $this->assertEquals('another property', $result->anotherProperty); + } + + public function testValuePropertyMiddleware() + { + $config = new AutoMapperConfig(); + $config->registerMiddlewares(new ValuePropertyMiddleware('property middleware value', 'name')); + $config->registerMapping(CompleteSource::class, Destination::class); + $mapper = new AutoMapper($config); + $source = new CompleteSource('a name', 'another property'); + + /** @var Destination $result */ + $result = $mapper->map($source, Destination::class); + + $this->assertEquals('property middleware value', $result->name); + $this->assertEquals('another property', $result->anotherProperty); + } + + public function testValueMapperDefaultMiddleware() + { + $config = new AutoMapperConfig(); + $config->registerMiddlewares(new ValueMapperDefaultMiddleware('mapper middleware value', 'name')); + $config->registerMapping(CompleteSource::class, Destination::class); + $mapper = new AutoMapper($config); + $source = new CompleteSource('a name', 'another property'); + + /** @var Destination $result */ + $result = $mapper->map($source, Destination::class); + + $this->assertEquals('mapper middleware value', $result->name); + $this->assertNull($result->anotherProperty); + } + + public function testValuePropertyDefaultMiddleware() + { + $config = new AutoMapperConfig(); + $config->registerMiddlewares(new ValuePropertyDefaultMiddleware('property middleware value', 'name')); + $config->registerMapping(CompleteSource::class, Destination::class); + $mapper = new AutoMapper($config); + $source = new CompleteSource('a name', 'another property'); + + /** @var Destination $result */ + $result = $mapper->map($source, Destination::class); + + $this->assertEquals('property middleware value', $result->name); + $this->assertNull($result->anotherProperty); + } + + public function testAppendMapperMiddleware() + { + $config = new AutoMapperConfig(); + $config->registerMiddlewares(new AppendMapperMiddleware(' (value)', 'name')); + $config->registerMapping(CompleteSource::class, Destination::class); + $mapper = new AutoMapper($config); + $source = new CompleteSource('a name'); + + /** @var Destination $result */ + $result = $mapper->map($source, Destination::class); + + $this->assertEquals('a name (value)', $result->name); + } + + public function testManyMiddlewares() + { + $config = new AutoMapperConfig(); + $config->registerMiddlewares( + new PrependMapperMiddleware('[before]', 'name'), + new ValuePropertyMiddleware('mapper middleware value', 'name'), + new PrependPropertyMiddleware('[before-property]', 'name'), + new AppendPropertyMiddleware('[after-property]', 'name'), + new AppendMapperMiddleware('[after]', 'name') + ); + $config->registerMapping(CompleteSource::class, Destination::class); + $mapper = new AutoMapper($config); + $source = new CompleteSource('a name', 'another property'); + + /** @var Destination $result */ + $result = $mapper->map($source, Destination::class); + + $this->assertEquals('[before][before-property]mapper middleware value[after-property][after]', $result->name); + $this->assertEquals('another property', $result->anotherProperty); + } + + public function testAnswerToUniverse() + { + $config = new AutoMapperConfig(); + $config->registerMiddlewares(new AnwserToUniverseMiddleware()); + $config->registerMapping(Employee::class, EmployeeDto::class); + $config->registerMapping( + \AutoMapperPlus\Test\Models\SimilarPropertyNames\Source::class, + \AutoMapperPlus\Test\Models\SimilarPropertyNames\Destination::class + ); + + $mapper = new AutoMapper($config); + $source1 = new Employee(NULL, 'John', 'Doe', 1980); + $source2 = new \AutoMapperPlus\Test\Models\SimilarPropertyNames\Source(NULL, NULL); + + /** @var EmployeeDto $result1 */ + $result1 = $mapper->map($source1, EmployeeDto::class); + $this->assertEquals(42, $result1->id); + $this->assertEquals('John', $result1->firstName); + $this->assertEquals('Doe', $result1->lastName); + + /** + * @var \AutoMapperPlus\Test\Models\SimilarPropertyNames\Destination $result2 + */ + $result2 = $mapper->map($source2, \AutoMapperPlus\Test\Models\SimilarPropertyNames\Destination::class); + $this->assertEquals(42, $result2->id); + $this->assertNull($result2->second_id); + + } +}