Skip to content

Commit

Permalink
Add middleware feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Toilal committed Sep 12, 2019
1 parent a0a7d7a commit 8ba45d9
Show file tree
Hide file tree
Showing 22 changed files with 862 additions and 25 deletions.
43 changes: 42 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
<?php

class AnwserToUniverseMiddleware implements PropertyMiddleware
{
public function mapProperty($propertyName,
$source,
$destination,
AutoMapperInterface $mapper,
MappingInterface $mapping,
MappingOperationInterface $operation,
array $context,
callable $next)
{
if ($propertyName === 'id') {
$defaultValue = $mapping->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
Expand Down Expand Up @@ -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*
41 changes: 18 additions & 23 deletions src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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->getConfiguration()->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;
}

Expand Down
78 changes: 78 additions & 0 deletions src/Configuration/AutoMapperConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand All @@ -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
*/
Expand All @@ -30,6 +53,8 @@ public function __construct(callable $configurator = null)
if ($configurator !== null) {
$configurator($this->options);
}
$this->defaultMapperMiddleware = new DefaultMapperMiddleware();
$this->defaultPropertyMiddleware = new DefaultPropertyMiddleware();
}

/**
Expand Down Expand Up @@ -202,11 +227,64 @@ 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
*/
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;
}
}
39 changes: 39 additions & 0 deletions src/Configuration/AutoMapperConfigInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
*
Expand Down Expand Up @@ -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();
}
80 changes: 80 additions & 0 deletions src/Middleware/DefaultMapperMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
<?php


namespace AutoMapperPlus\Middleware;


use AutoMapperPlus\AutoMapper;
use AutoMapperPlus\AutoMapperInterface;
use AutoMapperPlus\Configuration\MappingInterface;
use AutoMapperPlus\MappingOperation\ContextAwareOperation;
use AutoMapperPlus\MappingOperation\MapperAwareOperation;

class DefaultMapperMiddleware implements MapperMiddleware, DefaultMiddleware
{
protected function doMap($source, $destination, AutoMapperInterface $mapper, MappingInterface $mapping, array $context)
{
$propertyNames = $mapping->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($mapper->getConfiguration()->getPropertyMiddlewares()) 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]);
}
}
14 changes: 14 additions & 0 deletions src/Middleware/DefaultMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php


namespace AutoMapperPlus\Middleware;


/**
* Marker interface for default middlewares.
*
* When registering a middleware marked with this interface, it will replace the default mapping behavior.
*/
interface DefaultMiddleware extends Middleware
{
}

0 comments on commit 8ba45d9

Please sign in to comment.