Skip to content

Commit

Permalink
feat(mapper): add a way to instantiate object in mapper with providers
Browse files Browse the repository at this point in the history
  • Loading branch information
joelwurtz committed Mar 24, 2024
1 parent 8e2e037 commit 64900f3
Show file tree
Hide file tree
Showing 26 changed files with 446 additions and 99 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [GH#78](https://github.com/jolicode/automapper/pull/78) Add MapTo & MapFrom listeners to bundle
- [GH#80](https://github.com/jolicode/automapper/pull/80) Add if feature to MapTo / MapFrom attributes
- [GH#81](https://github.com/jolicode/automapper/pull/81) Allow map to / from attribute in class when declaring a transformer and a name
- [GH#96](https://github.com/jolicode/automapper/pull/96) Add a way to instantiate the target object from external service using provider

### Changed
- [GH#56](https://github.com/jolicode/automapper/pull/56) Refactor metadata
Expand Down
1 change: 1 addition & 0 deletions docs/_nav.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- [Conditional mapping](mapping/conditional-mapping.md)
- [Groups](mapping/groups.md)
- [Transformer](mapping/transformer.md)
- [Provider](mapping/provider.md)
- [Mapping inheritance](mapping/inheritance.md)
- [Symfony Bundle](bundle/index.md)
- [Installation](bundle/installation.md)
Expand Down
1 change: 1 addition & 0 deletions docs/mapping/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ a `source` and a `target`.
- [Conditional mapping](conditional-mapping.md)
- [Groups](groups.md)
- [Transformer](transformer.md)
- [Provider](provider.md)
- [Mapping inheritance](inheritance.md)
76 changes: 76 additions & 0 deletions docs/mapping/provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# Provider

> [!WARNING]
> Providers are experimental and may change in the future.
Providers are a way to instantiate the `target` during the mapping process.

By default, the AutoMapper will try to instantiate the `target` object using the constructor, or without if not possible.
However, in some cases you may want to use a custom provider. Like fetch the object from the database, or use a factory.

In this case you can create a provider class that implements the `ProviderInterface` interface.

```php
use AutoMapper\Provider\ProviderInterface;

class MyProvider implements ProviderInterface
{
public function provide(string $targetType, mixed $source, array $context): object|null
{
return new $targetType();
}
}
```

You have to register this provider when you create the `AutoMapper` object.

```php
use AutoMapper\AutoMapper;

$autoMapper = AutoMapper::create(providers: [new MyProvider()]);
```

> [!NOTE]
> When using the Symfony Bundle version of the AutoMapper, you can use the `auto_mapper.provider` tag to register the provider.
> If you have autoconfiguration enabled, you do not need to register the provider manually as the tag will be automatically added.
Then you can use the `#[MapProvider]` attribute on top of the `target` class that you want to use this provider.

```php
use AutoMapper\Attribute\MapProvider;

#[MapProvider(provider: MyProvider::class)]
class Entity
{
}
```

> [!NOTE]
> When using the Symfony Bundle version of the AutoMapper, the provider will be the service id, which may be different
> from the class name.
Now, every time the `AutoMapper` needs to instantiate the `Entity` class, it will use the `MyProvider` class.

If you provider return `null`, the `AutoMapper` will try to instantiate the `target` object using the constructor, or without if not possible.

> [!NOTE]
> When using the `target_to_populate` option in the `context` array, the `AutoMapper` will use this object instead of the
> one created by the provider.
### Early return

If you want to return the object from the provider without mapping the properties, you can return a
`AutoMapper\Provider\EarlyReturn` object from the `provide` method with the object you want to return inside.

```php
use AutoMapper\Provider\EarlyReturn;
use AutoMapper\Provider\ProviderInterface;

class MyProvider implements ProviderInterface
{
public function provide(string $targetType, mixed $source, array $context): object|null
{
return new EarlyReturn(new $targetType());
}
}
```
11 changes: 11 additions & 0 deletions docs/mapping/transformer.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,13 @@ use AutoMapper\AutoMapper;
$autoMapper = AutoMapper::create(propertyTransformers: [new UrlTransformer($urlGenerator)]);
```

> [!NOTE]
> When using the Symfony Bundle version of the AutoMapper, you can use the `automapper.property_transformer` tag to
> register the transformer.
>
> If you have autoconfiguration enabled, you do not need to register the transformer manually as the tag will be
> automatically added.
Then you can use it in the `transformer` argument.

```php
Expand All @@ -104,6 +111,10 @@ class Source
}
```

> [!NOTE]
> When using the Symfony Bundle version of the AutoMapper, the transformer will be the service id, which may be different
> from the class name.
### Automatically apply custom transformers

You may want to automatically apply a custom transformer given a specific condition.
Expand Down
19 changes: 19 additions & 0 deletions src/Attribute/MapProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace AutoMapper\Attribute;

#[\Attribute(\Attribute::IS_REPEATABLE | \Attribute::TARGET_CLASS)]
final readonly class MapProvider
{
/**
* @param ?string $source from which source type this provider should apply
* @param string $provider the provider class name or service identifier
*/
public function __construct(
public string $provider,
public ?string $source = null,
) {
}
}
22 changes: 14 additions & 8 deletions src/AutoMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use AutoMapper\Loader\EvalLoader;
use AutoMapper\Loader\FileLoader;
use AutoMapper\Metadata\MetadataRegistry;
use AutoMapper\Provider\ProviderInterface;
use AutoMapper\Provider\ProviderRegistry;
use AutoMapper\Symfony\ExpressionLanguageProvider;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry;
Expand Down Expand Up @@ -46,6 +48,7 @@ public function __construct(
private readonly ClassLoaderInterface $classLoader,
private readonly PropertyTransformerRegistry $propertyTransformerRegistry,
private readonly MetadataRegistry $metadataRegistry,
private readonly ProviderRegistry $providerRegistry,
private readonly ?ExpressionLanguageProvider $expressionLanguageProvider = null,
) {
}
Expand Down Expand Up @@ -74,15 +77,15 @@ public function getMapper(string $source, string $target): MapperInterface
}

/** @var GeneratedMapper<Source, Target>|GeneratedMapper<array<mixed>, Target>|GeneratedMapper<Source, array<mixed>> $mapper */
$mapper = new $className();
$this->mapperRegistry[$className] = $mapper;
$mapper = new $className(
$this->propertyTransformerRegistry,
$this->providerRegistry,
$this->expressionLanguageProvider,
);

$mapper->injectMappers($this);
$mapper->setPropertyTransformers($this->propertyTransformerRegistry->getPropertyTransformers());
$this->mapperRegistry[$className] = $mapper;

if (null !== $this->expressionLanguageProvider) {
$mapper->setExpressionLanguageProvider($this->expressionLanguageProvider);
}
$mapper->registerMappers($this);

/** @var GeneratedMapper<Source, Target>|GeneratedMapper<array<mixed>, Target>|GeneratedMapper<Source, array<mixed>> */
return $this->mapperRegistry[$className];
Expand Down Expand Up @@ -118,6 +121,7 @@ public function map(array|object $source, string|array|object $target, array $co

/**
* @param TransformerFactoryInterface[] $transformerFactories
* @param ProviderInterface[] $providers
* @param iterable<string, PropertyTransformerInterface> $propertyTransformers
*/
public static function create(
Expand All @@ -128,6 +132,7 @@ public static function create(
iterable $propertyTransformers = [],
ExpressionLanguageProvider $expressionLanguageProvider = null,
EventDispatcherInterface $eventDispatcher = new EventDispatcher(),
iterable $providers = [],
): self {
if (class_exists(AttributeLoader::class)) {
$loaderClass = new AttributeLoader();
Expand All @@ -153,6 +158,7 @@ public static function create(
}

$customTransformerRegistry = new PropertyTransformerRegistry($propertyTransformers);
$providerRegistry = new ProviderRegistry($providers);
$metadataRegistry = MetadataRegistry::create($configuration, $customTransformerRegistry, $transformerFactories, $classMetadataFactory, $nameConverter, $expressionLanguage, $eventDispatcher);

$mapperGenerator = new MapperGenerator(
Expand All @@ -167,6 +173,6 @@ public static function create(
$loader = new FileLoader($mapperGenerator, $metadataRegistry, $cacheDirectory);
}

return new self($loader, $customTransformerRegistry, $metadataRegistry, $expressionLanguageProvider);
return new self($loader, $customTransformerRegistry, $metadataRegistry, $providerRegistry, $expressionLanguageProvider);
}
}
3 changes: 2 additions & 1 deletion src/Event/GenerateMapperEvent.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ final class GenerateMapperEvent
*/
public function __construct(
public readonly MapperMetadata $mapperMetadata,
public $properties = [],
public array $properties = [],
public ?string $provider = null,
) {
}
}
53 changes: 53 additions & 0 deletions src/EventListener/MapProviderListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace AutoMapper\EventListener;

use AutoMapper\Attribute\MapProvider;
use AutoMapper\Event\GenerateMapperEvent;
use AutoMapper\Exception\BadMapDefinitionException;

class MapProviderListener
{
public function __construct()
{
}

public function __invoke(GenerateMapperEvent $event): void
{
if (!$event->mapperMetadata->targetReflectionClass) {
return;
}

$attributes = $event->mapperMetadata->targetReflectionClass->getAttributes(MapProvider::class);

if (0 === \count($attributes)) {
return;
}

$provider = null;
$defaultMapProvider = null;

foreach ($attributes as $attribute) {
/** @var MapProvider $mapProvider */
$mapProvider = $attribute->newInstance();

if ($mapProvider->source === null) {
if ($defaultMapProvider !== null) {
throw new BadMapDefinitionException(sprintf('multiple default providers found for class "%s"', $event->mapperMetadata->targetReflectionClass->getName()));
}

$defaultMapProvider = $mapProvider->provider;
} elseif ($mapProvider->source === $event->mapperMetadata->source) {
if ($provider !== null) {
throw new BadMapDefinitionException(sprintf('multiple providers found for class "%s"', $event->mapperMetadata->targetReflectionClass->getName()));
}

$provider = $mapProvider->provider;
}
}

$event->provider ??= $provider ?? $defaultMapProvider;
}
}
59 changes: 18 additions & 41 deletions src/GeneratedMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

namespace AutoMapper;

use AutoMapper\Provider\ProviderRegistry;
use AutoMapper\Symfony\ExpressionLanguageProvider;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerInterface;
use AutoMapper\Transformer\PropertyTransformer\PropertyTransformerRegistry;

/**
* Class derived for each generated mapper.
Expand All @@ -19,6 +20,22 @@
*/
abstract class GeneratedMapper implements MapperInterface
{
final public function __construct(
protected PropertyTransformerRegistry $transformerRegistry,
protected ProviderRegistry $providerRegistry,
protected ?ExpressionLanguageProvider $expressionLanguageProvider = null,
) {
$this->initialize();
}

public function initialize(): void
{
}

public function registerMappers(AutoMapperRegistryInterface $registry): void
{
}

/** @var array<string, MapperInterface<object, object>|MapperInterface<object, array<mixed>>|MapperInterface<array<mixed>, object>> */
protected array $mappers = [];

Expand All @@ -33,44 +50,4 @@ abstract class GeneratedMapper implements MapperInterface

/** @var Target|\ReflectionClass<object> */
protected mixed $cachedTarget;

/** @var null|callable(mixed value): mixed */
protected mixed $circularReferenceHandler = null;

protected ?int $circularReferenceLimit = null;

/** @var array<string, PropertyTransformerInterface> */
protected array $transformers = [];

protected ?ExpressionLanguageProvider $expressionLanguageProvider = null;

/**
* Inject sub mappers.
*/
public function injectMappers(AutoMapperRegistryInterface $autoMapperRegistry): void
{
}

public function setCircularReferenceHandler(?callable $circularReferenceHandler): void
{
$this->circularReferenceHandler = $circularReferenceHandler;
}

public function setCircularReferenceLimit(?int $circularReferenceLimit): void
{
$this->circularReferenceLimit = $circularReferenceLimit;
}

/**
* @param array<string, PropertyTransformerInterface> $transformers
*/
public function setPropertyTransformers(array $transformers): void
{
$this->transformers = $transformers;
}

public function setExpressionLanguageProvider(ExpressionLanguageProvider $expressionLanguageProvider): void
{
$this->expressionLanguageProvider = $expressionLanguageProvider;
}
}
Loading

0 comments on commit 64900f3

Please sign in to comment.