Skip to content

Commit

Permalink
Add middleware feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Toilal committed Aug 9, 2019
1 parent 88145c3 commit e2f20d5
Show file tree
Hide file tree
Showing 22 changed files with 862 additions and 37 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 @@ -804,6 +805,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 @@ -915,7 +956,7 @@ where needed, without needing to change the code that uses the mapper.
- [ ] 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*
55 changes: 27 additions & 28 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 @@ -59,8 +59,7 @@ public function map($source, string $destinationClass, array $context = [])

if (\is_object($source)) {
$sourceClass = \get_class($source);
}
else {
} else {
$sourceClass = \gettype($source);
if ($sourceClass !== DataType::ARRAY) {
throw UnsupportedSourceTypeException::fromType($sourceClass);
Expand All @@ -82,15 +81,13 @@ public function map($source, string $destinationClass, array $context = [])
$this,
$context
);
}
elseif (interface_exists($destinationClass)) {
} elseif (interface_exists($destinationClass)) {
// If we're mapping to an interface a valid custom constructor has
// to be provided. Otherwise we can't know what to do.
$message = 'Mapping to an interface is not possible. Please '
. 'provide a concrete class or use mapToObject instead.';
throw new AutoMapperPlusException($message);
}
else {
} else {
$destinationObject = new $destinationClass;
}

Expand All @@ -106,8 +103,9 @@ public function mapMultiple(
$sourceCollection,
string $destinationClass,
array $context = []
): array {
if(!is_iterable($sourceCollection)){
): array
{
if (!is_iterable($sourceCollection)) {
throw new InvalidArgumentException(
'The collection provided should be iterable.'
);
Expand All @@ -128,8 +126,7 @@ public function mapToObject($source, $destination, array $context = [])
{
if (\is_object($source)) {
$sourceClass = \get_class($source);
}
else {
} else {
$sourceClass = \gettype($source);
if ($sourceClass !== DataType::ARRAY) {
throw UnsupportedSourceTypeException::fromType($sourceClass);
Expand Down Expand Up @@ -164,7 +161,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 @@ -178,25 +175,26 @@ protected function doMap(
$destination,
MappingInterface $mapping,
array $context = []
) {
$propertyNames = $mapping->getTargetProperties($destination, $source);
foreach ($propertyNames as $propertyName) {
$mappingOperation = $mapping->getMappingOperationFor($propertyName);
)
{
$mapper = $this;

if ($mappingOperation instanceof MapperAwareOperation) {
$mappingOperation->setMapper($this);
}
if ($mappingOperation instanceof ContextAwareOperation) {
$mappingOperation->setContext($context);
}
$this->autoMapperConfig->getDefaultMapperMiddleware()->map($source, $destination, $mapper, $mapping, $context, function () {
});

$mappingOperation->mapProperty(
$propertyName,
$source,
$destination
);
$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 All @@ -218,7 +216,8 @@ protected function getMapping
(
string $sourceClass,
string $destinationClass
): MappingInterface {
): MappingInterface
{
$mapping = $this->autoMapperConfig->getMappingFor(
$sourceClass,
$destinationClass
Expand Down
97 changes: 90 additions & 7 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 All @@ -38,7 +63,8 @@ public function __construct(callable $configurator = null)
public function hasMappingFor(
string $sourceClassName,
string $destinationClassName
): bool {
): bool
{
$mapping = $this->getMappingFor(
$sourceClassName,
$destinationClassName
Expand All @@ -53,7 +79,8 @@ public function hasMappingFor(
public function getMappingFor(
string $sourceClassName,
string $destinationClassName
): ?MappingInterface {
): ?MappingInterface
{
// Check for an exact match before we try parent classes.
foreach ($this->mappings as $mapping) {
$isExactMatch = $mapping->getSourceClassName() === $sourceClassName
Expand Down Expand Up @@ -120,11 +147,12 @@ protected function getMostSpecificCandidate(
array $candidates,
string $sourceClassName,
string $destinationClassName
): ?MappingInterface {
): ?MappingInterface
{
$lowestDistance = PHP_INT_MAX;
$selectedCandidate = null;
/** @var MappingInterface $candidate */
foreach($candidates as $candidate) {
foreach ($candidates as $candidate) {
$sourceDistance = $this->getClassDistance(
$sourceClassName,
$candidate->getSourceClassName()
Expand Down Expand Up @@ -154,14 +182,15 @@ protected function getMostSpecificCandidate(
protected function getClassDistance(
string $childClass,
string $parentClass
): int {
): int
{
if ($childClass === $parentClass) {
return 0;
}

$result = 0;
$childParents = class_parents($childClass, true);
foreach($childParents as $childParent) {
foreach ($childParents as $childParent) {
$result++;
if ($childParent === $parentClass) {
return $result;
Expand Down Expand Up @@ -194,7 +223,8 @@ protected function getClassDistance(
public function registerMapping(
string $sourceClassName,
string $destinationClassName
): MappingInterface {
): MappingInterface
{
$mapping = new Mapping(
$sourceClassName,
$destinationClassName,
Expand All @@ -205,11 +235,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;
}
}
Loading

0 comments on commit e2f20d5

Please sign in to comment.