From fe334e53c1e7db69dd2366f1f85ec7e7c1873329 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Sun, 5 May 2024 13:07:46 +0200 Subject: [PATCH] feat(attribute): allow multiple source/target, allow overriding attribute with priority system --- docs/mapping/attributes.md | 31 +++++++++++++++++-- src/Attribute/MapFrom.php | 17 +++++----- src/Attribute/MapTo.php | 17 +++++----- src/Event/PropertyMetadataEvent.php | 1 + src/EventListener/MapFromListener.php | 9 ++++-- src/EventListener/MapToListener.php | 9 ++++-- tests/AutoMapperMapToTest.php | 12 ++++--- .../MapTo/{BadMapTo.php => PriorityMapTo.php} | 6 ++-- 8 files changed, 70 insertions(+), 32 deletions(-) rename tests/Fixtures/MapTo/{BadMapTo.php => PriorityMapTo.php} (59%) diff --git a/docs/mapping/attributes.md b/docs/mapping/attributes.md index da7e12ff..4fdc587a 100644 --- a/docs/mapping/attributes.md +++ b/docs/mapping/attributes.md @@ -69,5 +69,32 @@ class EntityDto } ``` -> [!WARNING] -> If multiple `#[MapTo]` and/or `#[MapFrom]` attributes target the same property an exception will be thrown. +You can also pass an array to the `target` or `source` argument to specify configuration for multiple targets or sources. +```php +class EntityDto +{ + #[MapFrom(source: Entity::class, property: 'title')] + #[MapFrom(source: 'array', property: 'name')] + public string $name; + + #[MapFrom(source: [Entity::class, 'array'], property: 'bar')] + public string $foo; +} +``` + +In case there is multiple attributes that match the same target (not source), you can use the `priority` argument +to specify which one should be used first. The default priority is `0`. + +```php +class Entity +{ + #[MapTo(ignore: true)] + public string $title; +} + +class EntityDto +{ + #[MapFrom(source: Entity::class, ignore: false, priority: 10)] + public string $title; +} +``` \ No newline at end of file diff --git a/src/Attribute/MapFrom.php b/src/Attribute/MapFrom.php index da24e144..297aee12 100644 --- a/src/Attribute/MapFrom.php +++ b/src/Attribute/MapFrom.php @@ -11,22 +11,23 @@ final readonly class MapFrom { /** - * @param class-string|'array'|null $source The specific source class name or array. If null this attribute will be used for all source classes. - * @param string|null $property The source property name. If null, the target property name will be used. - * @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used. - * @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping - * @param bool|null $ignore if true, the property will be ignored during mapping - * @param string|null $if The condition to map the property, using the expression language - * @param string[]|null $groups The groups to map the property + * @param class-string|'array'|array|'array'>|null $source The specific source class name or array. If null this attribute will be used for all source classes. + * @param string|null $property The source property name. If null, the target property name will be used. + * @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used. + * @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping + * @param bool|null $ignore if true, the property will be ignored during mapping + * @param string|null $if The condition to map the property, using the expression language + * @param string[]|null $groups The groups to map the property */ public function __construct( - public ?string $source = null, + public string|array|null $source = null, public ?string $property = null, public ?int $maxDepth = null, public mixed $transformer = null, public ?bool $ignore = null, public ?string $if = null, public ?array $groups = null, + public int $priority = 0, ) { } } diff --git a/src/Attribute/MapTo.php b/src/Attribute/MapTo.php index 511db8c3..6b6e9838 100644 --- a/src/Attribute/MapTo.php +++ b/src/Attribute/MapTo.php @@ -11,22 +11,23 @@ final readonly class MapTo { /** - * @param class-string|'array'|null $target The specific target class name or array. If null this attribute will be used for all target classes. - * @param string|null $property The target property name. If null, the source property name will be used. - * @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used. - * @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping - * @param bool|null $ignore if true, the property will be ignored during mapping - * @param string|null $if The condition to map the property, using the expression language - * @param string[]|null $groups The groups to map the property + * @param class-string|'array'|array|'array'>|null $target The specific target class name or array. If null this attribute will be used for all target classes. + * @param string|null $property The target property name. If null, the source property name will be used. + * @param int|null $maxDepth The maximum depth of the mapping. If null, the default max depth will be used. + * @param string|callable(mixed $value, object $object): mixed|null $transformer A transformer id or a callable that transform the value during mapping + * @param bool|null $ignore if true, the property will be ignored during mapping + * @param string|null $if The condition to map the property, using the expression language + * @param string[]|null $groups The groups to map the property */ public function __construct( - public ?string $target = null, + public string|array|null $target = null, public ?string $property = null, public ?int $maxDepth = null, public mixed $transformer = null, public ?bool $ignore = null, public ?string $if = null, public ?array $groups = null, + public int $priority = 0, ) { } } diff --git a/src/Event/PropertyMetadataEvent.php b/src/Event/PropertyMetadataEvent.php index d45054ec..0e11d310 100644 --- a/src/Event/PropertyMetadataEvent.php +++ b/src/Event/PropertyMetadataEvent.php @@ -28,6 +28,7 @@ public function __construct( public ?string $if = null, public ?array $groups = null, public ?bool $disableGroupsCheck = null, + public int $priority = 0, ) { } } diff --git a/src/EventListener/MapFromListener.php b/src/EventListener/MapFromListener.php index 691fbe1d..59af1ac8 100644 --- a/src/EventListener/MapFromListener.php +++ b/src/EventListener/MapFromListener.php @@ -62,7 +62,9 @@ public function __invoke(GenerateMapperEvent $event): void private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapFrom, string $property): void { - if ($mapFrom->source !== null && $event->mapperMetadata->source !== $mapFrom->source) { + $sources = null === $mapFrom->source ? null : (\is_array($mapFrom->source) ? $mapFrom->source : [$mapFrom->source]); + + if ($sources !== null && !\in_array($event->mapperMetadata->source, $sources, true)) { return; } @@ -79,10 +81,11 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF ignoreReason: $mapFrom->ignore === true ? 'Property is ignored by MapFrom Attribute on Target' : null, if: $mapFrom->if, groups: $mapFrom->groups, + priority: $mapFrom->priority, ); - if (\array_key_exists($property->target->property, $event->properties)) { - throw new BadMapDefinitionException(sprintf('There is already a MapTo or MapFrom attribute with target "%s" in class "%s" or class "%s".', $property->target->property, $event->mapperMetadata->source, $event->mapperMetadata->target)); + if (\array_key_exists($property->target->property, $event->properties) && $event->properties[$property->target->property]->priority >= $property->priority) { + return; } $event->properties[$property->target->property] = $property; diff --git a/src/EventListener/MapToListener.php b/src/EventListener/MapToListener.php index 9850a083..6e98f28b 100644 --- a/src/EventListener/MapToListener.php +++ b/src/EventListener/MapToListener.php @@ -63,7 +63,9 @@ public function __invoke(GenerateMapperEvent $event): void private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo, string $property): void { - if ($mapTo->target !== null && $event->mapperMetadata->target !== $mapTo->target) { + $targets = null === $mapTo->target ? null : (\is_array($mapTo->target) ? $mapTo->target : [$mapTo->target]); + + if ($targets !== null && !\in_array($event->mapperMetadata->target, $targets, true)) { return; } @@ -80,10 +82,11 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo, ignoreReason: $mapTo->ignore === true ? 'Property is ignored by MapTo Attribute on Source' : null, if: $mapTo->if, groups: $mapTo->groups, + priority: $mapTo->priority, ); - if (\array_key_exists($property->target->property, $event->properties)) { - throw new BadMapDefinitionException(sprintf('There is already a MapTo attribute with target "%s" in class "%s".', $property->target->property, $event->mapperMetadata->source)); + if (\array_key_exists($property->target->property, $event->properties) && $event->properties[$property->target->property]->priority >= $property->priority) { + return; } $event->properties[$property->target->property] = $property; diff --git a/tests/AutoMapperMapToTest.php b/tests/AutoMapperMapToTest.php index e42cb6db..050471cd 100644 --- a/tests/AutoMapperMapToTest.php +++ b/tests/AutoMapperMapToTest.php @@ -7,10 +7,10 @@ use AutoMapper\Exception\BadMapDefinitionException; use AutoMapper\MapperContext; use AutoMapper\Symfony\ExpressionLanguageProvider; -use AutoMapper\Tests\Fixtures\MapTo\BadMapTo; use AutoMapper\Tests\Fixtures\MapTo\BadMapToTransformer; use AutoMapper\Tests\Fixtures\MapTo\Bar; use AutoMapper\Tests\Fixtures\MapTo\FooMapTo; +use AutoMapper\Tests\Fixtures\MapTo\PriorityMapTo; use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\FooDependency; use AutoMapper\Tests\Fixtures\Transformer\CustomTransformer\TransformerWithDependency; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -114,12 +114,14 @@ public function testMapFromArray() $this->assertSame('', $bar->getB()); } - public function testBadDefinitionOnSameTargetProperty() + public function testPriority() { - $foo = new BadMapTo('foo'); + $foo = new PriorityMapTo('foo'); - $this->expectException(BadMapDefinitionException::class); - $this->autoMapper->map($foo, 'array'); + $result = $this->autoMapper->map($foo, 'array'); + + self::assertArrayHasKey('foo', $result); + self::assertSame('foo', $result['foo']); } public function testBadDefinitionOnTransformer() diff --git a/tests/Fixtures/MapTo/BadMapTo.php b/tests/Fixtures/MapTo/PriorityMapTo.php similarity index 59% rename from tests/Fixtures/MapTo/BadMapTo.php rename to tests/Fixtures/MapTo/PriorityMapTo.php index a71652e6..eaa759b5 100644 --- a/tests/Fixtures/MapTo/BadMapTo.php +++ b/tests/Fixtures/MapTo/PriorityMapTo.php @@ -6,11 +6,11 @@ use AutoMapper\Attribute\MapTo; -class BadMapTo +class PriorityMapTo { public function __construct( - #[MapTo('array', ignore: true)] - #[MapTo('array', ignore: false)] + #[MapTo('array', ignore: true, priority: 0)] + #[MapTo('array', ignore: false, priority: 10)] public string $foo ) { }