Skip to content

Commit a2cb66d

Browse files
committed
Add middleware feature
1 parent 88145c3 commit a2cb66d

22 files changed

+862
-37
lines changed

README.md

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Transfers data from one object to another, allowing custom mapping operations.
3131
* [The concept of object crates](#the-concept-of-object-crates)
3232
* [Mapping with arrays](#mapping-with-arrays)
3333
* [Using a custom mapper](#using-a-custom-mapper)
34+
* [Using middlewares](#using-middlewares)
3435
* [Adding context](#adding-context)
3536
* [Misc](#misc)
3637
* [Similar libraries](#similar-libraries)
@@ -804,6 +805,46 @@ $employee = new Employee(10, 'John', 'Doe', 1980);
804805
$result = $mapper->map($employee, EmployeeDto::class);
805806
```
806807

808+
### Using middlewares
809+
You can register middlewares to customize how automapper works internally and define
810+
global behaviors.
811+
812+
The following example will set 42 to any `id` property that would have been `null`.
813+
814+
```php
815+
<?php
816+
817+
class AnwserToUniverseMiddleware implements PropertyMiddleware
818+
{
819+
public function mapProperty($propertyName,
820+
$source,
821+
$destination,
822+
AutoMapperInterface $mapper,
823+
MappingInterface $mapping,
824+
MappingOperationInterface $operation,
825+
array $context,
826+
callable $next)
827+
{
828+
if ($propertyName === 'id') {
829+
$defaultValue = $mapping->getOptions()->getPropertyReader()->getProperty($destination, $propertyName);
830+
if ($defaultValue === NULL) {
831+
$mapping->getOptions()->getPropertyWriter()->setProperty($destination, $propertyName, 42);
832+
}
833+
}
834+
$next();
835+
}
836+
}
837+
838+
$config->registerMiddlewares(new AnwserToUniverseMiddleware());
839+
$config->registerMapping(Employee::class, EmployeeDto::class);
840+
$mapper = new AutoMapper($config);
841+
842+
// The AutoMapper can now be used as usual, but your middleware will intercept some property mappings.
843+
$employee = new Employee(NULL, 'John', 'Doe', 1980);
844+
$result = $mapper->map($employee, EmployeeDto::class);
845+
echo $result->id; // => 42
846+
```
847+
807848
### Adding context
808849
Sometimes a mapping should behave differently based on the context. It is
809850
therefore possible to pass a third argument to the map methods to describe
@@ -915,7 +956,7 @@ where needed, without needing to change the code that uses the mapper.
915956
- [ ] Allow setting a maximum depth, see #14
916957
- [ ] Provide a NameResolver that accepts an array mapping, as an alternative to multiple `FromProperty`s
917958
- [ ] Make use of a decorated Symfony's `PropertyAccessor` (see [#16](https://github.com/mark-gerarts/automapper-plus/issues/16))
918-
- [ ] Allow adding of middleware to the mapper
959+
- [x] Allow adding of middleware to the mapper
919960
- [ ] Allow mapping *to* array
920961

921962
*[Version 2](https://github.com/mark-gerarts/automapper-plus/tree/2.0) is in the works, check there for new features as well*

src/AutoMapper.php

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
use AutoMapperPlus\Exception\InvalidArgumentException;
1010
use AutoMapperPlus\Exception\UnregisteredMappingException;
1111
use AutoMapperPlus\Exception\UnsupportedSourceTypeException;
12-
use AutoMapperPlus\MappingOperation\ContextAwareOperation;
1312
use AutoMapperPlus\MappingOperation\MapperAwareOperation;
13+
use AutoMapperPlus\Middleware\MapperMiddleware;
1414

1515
/**
1616
* Class AutoMapper
@@ -59,8 +59,7 @@ public function map($source, string $destinationClass, array $context = [])
5959

6060
if (\is_object($source)) {
6161
$sourceClass = \get_class($source);
62-
}
63-
else {
62+
} else {
6463
$sourceClass = \gettype($source);
6564
if ($sourceClass !== DataType::ARRAY) {
6665
throw UnsupportedSourceTypeException::fromType($sourceClass);
@@ -82,15 +81,13 @@ public function map($source, string $destinationClass, array $context = [])
8281
$this,
8382
$context
8483
);
85-
}
86-
elseif (interface_exists($destinationClass)) {
84+
} elseif (interface_exists($destinationClass)) {
8785
// If we're mapping to an interface a valid custom constructor has
8886
// to be provided. Otherwise we can't know what to do.
8987
$message = 'Mapping to an interface is not possible. Please '
9088
. 'provide a concrete class or use mapToObject instead.';
9189
throw new AutoMapperPlusException($message);
92-
}
93-
else {
90+
} else {
9491
$destinationObject = new $destinationClass;
9592
}
9693

@@ -106,8 +103,9 @@ public function mapMultiple(
106103
$sourceCollection,
107104
string $destinationClass,
108105
array $context = []
109-
): array {
110-
if(!is_iterable($sourceCollection)){
106+
): array
107+
{
108+
if (!is_iterable($sourceCollection)) {
111109
throw new InvalidArgumentException(
112110
'The collection provided should be iterable.'
113111
);
@@ -128,8 +126,7 @@ public function mapToObject($source, $destination, array $context = [])
128126
{
129127
if (\is_object($source)) {
130128
$sourceClass = \get_class($source);
131-
}
132-
else {
129+
} else {
133130
$sourceClass = \gettype($source);
134131
if ($sourceClass !== DataType::ARRAY) {
135132
throw UnsupportedSourceTypeException::fromType($sourceClass);
@@ -164,7 +161,7 @@ public function mapToObject($source, $destination, array $context = [])
164161
}
165162

166163
/**
167-
* Performs the actual transferring of properties.
164+
* Performs the actual transferring of properties, involving all matching mapper and property middleware.
168165
*
169166
* @param $source
170167
* @param $destination
@@ -178,25 +175,26 @@ protected function doMap(
178175
$destination,
179176
MappingInterface $mapping,
180177
array $context = []
181-
) {
182-
$propertyNames = $mapping->getTargetProperties($destination, $source);
183-
foreach ($propertyNames as $propertyName) {
184-
$mappingOperation = $mapping->getMappingOperationFor($propertyName);
178+
)
179+
{
180+
$mapper = $this;
185181

186-
if ($mappingOperation instanceof MapperAwareOperation) {
187-
$mappingOperation->setMapper($this);
188-
}
189-
if ($mappingOperation instanceof ContextAwareOperation) {
190-
$mappingOperation->setContext($context);
191-
}
182+
$this->autoMapperConfig->getDefaultMapperMiddleware()->map($source, $destination, $mapper, $mapping, $context, function () {
183+
});
192184

193-
$mappingOperation->mapProperty(
194-
$propertyName,
195-
$source,
196-
$destination
197-
);
185+
$map = function () {
186+
// NOOP
187+
};
188+
189+
foreach (array_reverse($this->getConfiguration()->getMapperMiddlewares()) as $middleware) {
190+
$map = function () use ($middleware, $source, $destination, $mapper, $mapping, $context, $map) {
191+
/** @var MapperMiddleware $middleware */
192+
return $middleware->map($source, $destination, $mapper, $mapping, $context, $map);
193+
};
198194
}
199195

196+
$map();
197+
200198
return $destination;
201199
}
202200

@@ -218,7 +216,8 @@ protected function getMapping
218216
(
219217
string $sourceClass,
220218
string $destinationClass
221-
): MappingInterface {
219+
): MappingInterface
220+
{
222221
$mapping = $this->autoMapperConfig->getMappingFor(
223222
$sourceClass,
224223
$destinationClass

src/Configuration/AutoMapperConfig.php

Lines changed: 90 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,13 @@
22

33
namespace AutoMapperPlus\Configuration;
44

5+
use AutoMapperPlus\Middleware\DefaultMapperMiddleware;
6+
use AutoMapperPlus\Middleware\DefaultMiddleware;
7+
use AutoMapperPlus\Middleware\DefaultPropertyMiddleware;
8+
use AutoMapperPlus\Middleware\MapperMiddleware;
9+
use AutoMapperPlus\Middleware\Middleware;
10+
use AutoMapperPlus\Middleware\PropertyMiddleware;
11+
512
/**
613
* Class AutoMapperConfig
714
*
@@ -14,6 +21,22 @@ class AutoMapperConfig implements AutoMapperConfigInterface
1421
*/
1522
private $mappings = [];
1623

24+
/** @var MapperMiddleware */
25+
private $defaultMapperMiddleware;
26+
27+
/** @var PropertyMiddleware */
28+
private $defaultPropertyMiddleware;
29+
30+
/**
31+
* @var MapperMiddleware[]
32+
*/
33+
private $mapperMiddlewares = [];
34+
35+
/**
36+
* @var PropertyMiddleware[]
37+
*/
38+
private $propertyMiddlewares = [];
39+
1740
/**
1841
* @var Options
1942
*/
@@ -30,6 +53,8 @@ public function __construct(callable $configurator = null)
3053
if ($configurator !== null) {
3154
$configurator($this->options);
3255
}
56+
$this->defaultMapperMiddleware = new DefaultMapperMiddleware();
57+
$this->defaultPropertyMiddleware = new DefaultPropertyMiddleware();
3358
}
3459

3560
/**
@@ -38,7 +63,8 @@ public function __construct(callable $configurator = null)
3863
public function hasMappingFor(
3964
string $sourceClassName,
4065
string $destinationClassName
41-
): bool {
66+
): bool
67+
{
4268
$mapping = $this->getMappingFor(
4369
$sourceClassName,
4470
$destinationClassName
@@ -53,7 +79,8 @@ public function hasMappingFor(
5379
public function getMappingFor(
5480
string $sourceClassName,
5581
string $destinationClassName
56-
): ?MappingInterface {
82+
): ?MappingInterface
83+
{
5784
// Check for an exact match before we try parent classes.
5885
foreach ($this->mappings as $mapping) {
5986
$isExactMatch = $mapping->getSourceClassName() === $sourceClassName
@@ -120,11 +147,12 @@ protected function getMostSpecificCandidate(
120147
array $candidates,
121148
string $sourceClassName,
122149
string $destinationClassName
123-
): ?MappingInterface {
150+
): ?MappingInterface
151+
{
124152
$lowestDistance = PHP_INT_MAX;
125153
$selectedCandidate = null;
126154
/** @var MappingInterface $candidate */
127-
foreach($candidates as $candidate) {
155+
foreach ($candidates as $candidate) {
128156
$sourceDistance = $this->getClassDistance(
129157
$sourceClassName,
130158
$candidate->getSourceClassName()
@@ -154,14 +182,15 @@ protected function getMostSpecificCandidate(
154182
protected function getClassDistance(
155183
string $childClass,
156184
string $parentClass
157-
): int {
185+
): int
186+
{
158187
if ($childClass === $parentClass) {
159188
return 0;
160189
}
161190

162191
$result = 0;
163192
$childParents = class_parents($childClass, true);
164-
foreach($childParents as $childParent) {
193+
foreach ($childParents as $childParent) {
165194
$result++;
166195
if ($childParent === $parentClass) {
167196
return $result;
@@ -194,7 +223,8 @@ protected function getClassDistance(
194223
public function registerMapping(
195224
string $sourceClassName,
196225
string $destinationClassName
197-
): MappingInterface {
226+
): MappingInterface
227+
{
198228
$mapping = new Mapping(
199229
$sourceClassName,
200230
$destinationClassName,
@@ -205,11 +235,64 @@ public function registerMapping(
205235
return $mapping;
206236
}
207237

238+
239+
public function registerMiddlewares(Middleware ...$middlewares): AutoMapperConfigInterface
240+
{
241+
foreach ($middlewares as $middleware) {
242+
if ($middleware instanceof MapperMiddleware) {
243+
$this->mapperMiddlewares[] = $middleware;
244+
if ($middleware instanceof DefaultMiddleware) {
245+
$this->defaultMapperMiddleware = $middleware;
246+
}
247+
}
248+
if ($middleware instanceof PropertyMiddleware) {
249+
$this->propertyMiddlewares[] = $middleware;
250+
if ($middleware instanceof DefaultMiddleware) {
251+
$this->defaultPropertyMiddleware = $middleware;
252+
}
253+
}
254+
}
255+
256+
return $this;
257+
}
258+
208259
/**
209260
* @inheritdoc
210261
*/
211262
public function getOptions(): Options
212263
{
213264
return $this->options;
214265
}
266+
267+
/**
268+
* @return PropertyMiddleware
269+
*/
270+
public function getDefaultPropertyMiddleware(): PropertyMiddleware
271+
{
272+
return $this->defaultPropertyMiddleware;
273+
}
274+
275+
/**
276+
* @return MapperMiddleware
277+
*/
278+
public function getDefaultMapperMiddleware(): MapperMiddleware
279+
{
280+
return $this->defaultMapperMiddleware;
281+
}
282+
283+
/**
284+
* @inheritdoc
285+
*/
286+
public function getMapperMiddlewares()
287+
{
288+
return $this->mapperMiddlewares;
289+
}
290+
291+
/**
292+
* @inheritdoc
293+
*/
294+
public function getPropertyMiddlewares()
295+
{
296+
return $this->propertyMiddlewares;
297+
}
215298
}

0 commit comments

Comments
 (0)