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

feat(mapper): add a way to instantiate object in mapper with providers #96

Merged
merged 1 commit into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [GH#84](https://github.com/jolicode/automapper/pull/84) Allow expression language for transformer and add provider for custom functions
- [GH#86](https://github.com/jolicode/automapper/pull/86) Bundle: Allow to use eval loader instead of file
- [GH#89](https://github.com/jolicode/automapper/pull/89) Add normalizer format in context, allow skipping group checking and remove registry interface from normalizer
- [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 @@ -12,6 +12,8 @@
use AutoMapper\Loader\FileLoader;
use AutoMapper\Metadata\MetadataFactory;
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 @@ -47,6 +49,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 @@ -75,15 +78,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 @@ -119,6 +122,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 @@ -129,6 +133,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 @@ -155,6 +160,7 @@ public static function create(

$customTransformerRegistry = new PropertyTransformerRegistry($propertyTransformers);
$metadataRegistry = new MetadataRegistry($configuration);
$providerRegistry = new ProviderRegistry($providers);
$metadataFactory = MetadataFactory::create(
$configuration,
$customTransformerRegistry,
Expand All @@ -178,6 +184,6 @@ public static function create(
$loader = new FileLoader($mapperGenerator, $metadataFactory, $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
Loading