Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add middleware feature #47

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
50 changes: 44 additions & 6 deletions 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 @@ -769,15 +770,12 @@ of custom code. This code would look a lot cleaner if put in its own class.
Another reason to resort to a custom mapper would be [performance](#performance).

It is therefore possible to specify a custom mapper class for a mapping. This
mapper has to implement the `MapperInterface`. For your convenience, a
`CustomMapper` class has been provided that implements this interface.
mapper has to implement the `DestinationMapperInterface`.

```php
<?php

// You can either extend the CustomMapper, or just implement the MapperInterface
// directly.
class EmployeeMapper extends CustomMapper
class EmployeeMapper implements DestinationMapperInterface
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that CustomMapper is deprecated, and map method of the AbstractMapper class won't be invoked anymore (object is constructed through the automapper configuration, and then passed to mapToObject method)

{
/**
* @param Employee $source
Expand Down Expand Up @@ -805,6 +803,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 +974,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*
68 changes: 31 additions & 37 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 @@ -88,10 +88,6 @@ public function map($source, string $destinationClass, array $context = [])
$context[self::DESTINATION_CLASS_CONTEXT] = $destinationClass;

$mapping = $this->getMapping($sourceClass, $destinationClass);
if ($mapping->providesCustomMapper()) {
return $this->getCustomMapper($mapping)->map($source, $destinationClass);
}

if ($mapping->hasCustomConstructor()) {
$destinationObject = $mapping->getCustomConstructor()(
$source,
Expand Down Expand Up @@ -167,14 +163,6 @@ public function mapToObject($source, $destination, array $context = [])
$this->push(self::DESTINATION_STACK_CONTEXT, $destination, $context);
try {
$mapping = $this->getMapping($sourceClass, $destinationClass);
if ($mapping->providesCustomMapper()) {
return $this->getCustomMapper($mapping)->mapToObject(
$source,
$destination,
$context
);
}

return $this->doMap(
$source,
$destination,
Expand All @@ -188,7 +176,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 +192,28 @@ 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;

if ($mapping->providesCustomMapper()) {
$this->getCustomMapper($mapping)->mapToObject($source, $destination, $context);
} else {
$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;
}

Expand Down Expand Up @@ -267,9 +254,9 @@ protected function getMapping
/**
* @param MappingInterface $mapping
*
* @return MapperInterface|null
* @return DestinationMapperInterface|null
*/
private function getCustomMapper(MappingInterface $mapping): ?MapperInterface
private function getCustomMapper(MappingInterface $mapping): ?DestinationMapperInterface
{
$customMapper = $mapping->getCustomMapper();

Expand All @@ -279,4 +266,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();
});
}
}
33 changes: 33 additions & 0 deletions src/ClassMapperInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace AutoMapperPlus;

use AutoMapperPlus\Exception\UnregisteredMappingException;

/**
* Interface ClassMapperInterface
*
* @package AutoMapperPlus
*/
interface ClassMapperInterface
{
/**
* Maps an object to an instance of class $to, provided a mapping is
* configured.
*
* @param array|object $source
* The source object.
* @param string $targetClass
* The target classname.
* @param array $context
* An arbitrary array of values that will be passed to supporting
* mapping operations (e.g. MapFrom) to alter their behaviour based on
* the context.
* This is not explicitly required on the interface yet to preserve
* backwards compatibility, but will be added in version 2.0.
* @return mixed
* An instance of class $to.
* @throws UnregisteredMappingException
*/
public function map($source, string $targetClass/**, array $context = [] */);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this is a completely new interface, we can actually make the $context part of the interface

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, the existing interface extends this one, disregard!

}
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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This way you can only register all middlewares at once. It might be interesting to add an addPropertyMiddleware and addMapperMiddleware method. Or maybe we can take a look at Guzzle's way of doing things and make use of a separate stack class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, i'll change this ! Sorry for the delay, i'm busy on one project right now, but I use this current fork for it and it really solves many use cases :).

I'll try to update the pull request tomorrow.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem at all; I took a long time to reply myself :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mark-gerarts I'm reading this pull request again, and it's not required to register all middlewares at once, because registerMiddlewares is chainable, and invoking registerMiddleswares won't remove already registered middlewares.

So you can add middlewares one by one, by calling registerMiddlewares as many times as you want.

Do you plan to merge this PR and release 2.0 ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I want to include this in release 2.0, which I aim to release this summer. As for the adding of middlewares: registerMiddlewares does indeed allow for chaining, but maybe It's useful to add some utility methods such as add. I'll look into this into detail once I start merging the PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @mark-gerarts, any ETA for this release ?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've set up a board for the release. The scope is rather limited, but my time is as well at the moment. To be honest, I have not really been able to work on the release the past months.

I should be able to finally work a bit on the release again the following weeks (:crossed_fingers:). The question is if it will be enough to finish all open tasks. I'll keep you posted once I've worked myself in again and have a better overview!

{
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;
}
}