From 6de41c7220058eee489f7b3231b5824245113ed7 Mon Sep 17 00:00:00 2001 From: Joel Wurtz Date: Tue, 2 Dec 2025 16:07:52 +0100 Subject: [PATCH] feat(type): allow to define source / target property type in attribute with a string --- CHANGELOG.md | 1 + docs/bundle/expression-language.md | 2 +- docs/bundle/migrate.md | 2 +- docs/mapping/index.md | 1 + docs/mapping/type.md | 34 +++++++++++++++++++++++++++ src/Attribute/MapFrom.php | 6 +++-- src/Attribute/MapTo.php | 6 +++-- src/EventListener/MapFromListener.php | 7 ++++-- src/EventListener/MapListener.php | 2 ++ src/EventListener/MapToListener.php | 7 ++++-- tests/AutoMapperMapToTest.php | 2 ++ tests/Fixtures/MapTo/FooMapTo.php | 3 ++- 12 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 docs/mapping/type.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a88ae3..d53341d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- [GH#304](https://github.com/jolicode/automapper/pull/304) ; Allow to override source and/or target property type - [GH#297](https://github.com/jolicode/automapper/pull/297) : Support PHP 8.5 and Symfony 8, this library now use the `TypeInfo` Component for types instead of PropertyInfo directly. - [BC Break] `PropertyTransformerSupportInterface` does not use a `TypesMatching` anymore, you can get the type directly from `SourcePropertyMetadata` or `TargetPropertyMetadata`. - Debug command now show the type of each property mapped, transformers will also display more information. diff --git a/docs/bundle/expression-language.md b/docs/bundle/expression-language.md index da3dc543..5e5fd5b4 100644 --- a/docs/bundle/expression-language.md +++ b/docs/bundle/expression-language.md @@ -11,7 +11,7 @@ The `env` function allow you to access the environment variables. ```php class Entity { - #[MapTo('array', if: "env('FEATURE_ENABLED')")] + #[MapTo(target: 'array', if: "env('FEATURE_ENABLED')")] public string $name; } ``` diff --git a/docs/bundle/migrate.md b/docs/bundle/migrate.md index 079d735e..cff875b8 100644 --- a/docs/bundle/migrate.md +++ b/docs/bundle/migrate.md @@ -80,7 +80,7 @@ For example, if you have a custom normalizer that add a `virtualProperty` the no to do the same thing. ```php -#[MapTo('array', property: 'virtualProperty', transformer: MyTransformer::class)] +#[MapTo(target: 'array', property: 'virtualProperty', transformer: MyTransformer::class)] class App\Entity\MyEntity { // ... diff --git a/docs/mapping/index.md b/docs/mapping/index.md index bb132236..b21029a0 100644 --- a/docs/mapping/index.md +++ b/docs/mapping/index.md @@ -12,6 +12,7 @@ a `source` and a `target`. - [Groups](groups.md) - [Transformer](transformer.md) - [Provider](provider.md) +- [Type](type.md) - [Mapping inheritance](inheritance.md) - [Identifier: mapping existing objects](identifier.md) - [DateTime format](date-time.md) diff --git a/docs/mapping/type.md b/docs/mapping/type.md new file mode 100644 index 00000000..54ab6eee --- /dev/null +++ b/docs/mapping/type.md @@ -0,0 +1,34 @@ +# Property Type + +When mapping properties, AutoMapper uses the property type to determine how to map the value from the source to the target. + +It works well when both source and target properties are object, but when mapping to or from a generic data structure +like an array or `\stdClass`, the property type is not available. + +In this case the type is, by default, transformed to a native PHP type (`int`, `float`, `string`, `bool`, `array`, `object`, `null`). + +You can override this behavior by specifying the `sourcePropertyType` or `targetPropertyType` argument in the +`#[MapTo]` or `#[MapFrom]` attributes. + +```php +class Entity +{ + #[MapTo(target: 'array', targetPropertyType: 'int'] + public string $number; +} +``` + +In this example, when mapping to an array, the `number` property will be converted to an `int` instead of a `string`. + +This can also be useful when mapping to an object type with an union type, but you want to force a specific type during the mapping. + +```php +class EntityDto +{ + #[MapTo(target: Entity:class, sourcePropertyType: 'int'] + private int|float $value; +} +``` + +In this example we consider, that the source property is always an `int`, so AutoMapper will never consider +the `float` type during the mapping. diff --git a/src/Attribute/MapFrom.php b/src/Attribute/MapFrom.php index e5ae6c9f..3e6205c0 100644 --- a/src/Attribute/MapFrom.php +++ b/src/Attribute/MapFrom.php @@ -23,6 +23,8 @@ * @param string|null $dateTimeFormat The date-time format to use when transforming this property * @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method * @param bool|null $identifier If true, the property will be used as an identifier + * @param Type|string|null $sourcePropertyType Override the source property type, where this property is mapped from + * @param Type|string|null $targetPropertyType Override the target property type, which in this case is the property type where the attribute is defined */ public function __construct( public string|array|null $source = null, @@ -36,8 +38,8 @@ public function __construct( public ?string $dateTimeFormat = null, public ?bool $extractTypesFromGetter = null, public ?bool $identifier = null, - public ?Type $sourceType = null, - public ?Type $targetType = null, + public string|Type|null $sourcePropertyType = null, + public string|Type|null $targetPropertyType = null, ) { } } diff --git a/src/Attribute/MapTo.php b/src/Attribute/MapTo.php index 5c85befd..567516da 100644 --- a/src/Attribute/MapTo.php +++ b/src/Attribute/MapTo.php @@ -23,6 +23,8 @@ * @param string|null $dateTimeFormat The date-time format to use when transforming this property * @param bool|null $extractTypesFromGetter If true, the types will be extracted from the getter method * @param bool|null $identifier If true, the property will be used as an identifier + * @param Type|string|null $sourcePropertyType Override the source property type, which in this case is the property type where the attribute is defined + * @param Type|string|null $targetPropertyType Override the target property type where this property is mapped to */ public function __construct( public string|array|null $target = null, @@ -36,8 +38,8 @@ public function __construct( public ?string $dateTimeFormat = null, public ?bool $extractTypesFromGetter = null, public ?bool $identifier = null, - public ?Type $sourceType = null, - public ?Type $targetType = null, + public string|Type|null $sourcePropertyType = null, + public string|Type|null $targetPropertyType = null, ) { } } diff --git a/src/EventListener/MapFromListener.php b/src/EventListener/MapFromListener.php index f4ae5cce..dee76722 100644 --- a/src/EventListener/MapFromListener.php +++ b/src/EventListener/MapFromListener.php @@ -68,8 +68,11 @@ private function addPropertyFromTarget(GenerateMapperEvent $event, MapFrom $mapF return; } - $sourceProperty = new SourcePropertyMetadata($mapFrom->property ?? $property, type: $mapFrom->sourceType); - $targetProperty = new TargetPropertyMetadata($property, type: $mapFrom->targetType); + $sourcePropertyType = \is_string($mapFrom->sourcePropertyType) ? $this->stringTypeResolver->resolve($mapFrom->sourcePropertyType) : $mapFrom->sourcePropertyType; + $targetPropertyType = \is_string($mapFrom->targetPropertyType) ? $this->stringTypeResolver->resolve($mapFrom->targetPropertyType) : $mapFrom->targetPropertyType; + + $sourceProperty = new SourcePropertyMetadata($mapFrom->property ?? $property, type: $sourcePropertyType); + $targetProperty = new TargetPropertyMetadata($property, type: $targetPropertyType); $propertyMetadata = new PropertyMetadataEvent( mapperMetadata: $event->mapperMetadata, diff --git a/src/EventListener/MapListener.php b/src/EventListener/MapListener.php index 87d4d0c6..dbc3e941 100644 --- a/src/EventListener/MapListener.php +++ b/src/EventListener/MapListener.php @@ -18,6 +18,7 @@ use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor; use Symfony\Component\String\Inflector\EnglishInflector; use Symfony\Component\String\Inflector\InflectorInterface; +use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver; /** * @internal @@ -28,6 +29,7 @@ public function __construct( private PropertyTransformerRegistry $propertyTransformerRegistry, private ExpressionLanguage $expressionLanguage, private InflectorInterface $inflector = new EnglishInflector(), + protected StringTypeResolver $stringTypeResolver = new StringTypeResolver(), ) { } diff --git a/src/EventListener/MapToListener.php b/src/EventListener/MapToListener.php index 888ca8c9..e99f4ca6 100644 --- a/src/EventListener/MapToListener.php +++ b/src/EventListener/MapToListener.php @@ -69,8 +69,11 @@ private function addPropertyFromSource(GenerateMapperEvent $event, MapTo $mapTo, return; } - $sourceProperty = new SourcePropertyMetadata($property, type: $mapTo->sourceType); - $targetProperty = new TargetPropertyMetadata($mapTo->property ?? $property, type: $mapTo->targetType); + $sourcePropertyType = \is_string($mapTo->sourcePropertyType) ? $this->stringTypeResolver->resolve($mapTo->sourcePropertyType) : $mapTo->sourcePropertyType; + $targetPropertyType = \is_string($mapTo->targetPropertyType) ? $this->stringTypeResolver->resolve($mapTo->targetPropertyType) : $mapTo->targetPropertyType; + + $sourceProperty = new SourcePropertyMetadata($property, type: $sourcePropertyType); + $targetProperty = new TargetPropertyMetadata($mapTo->property ?? $property, type: $targetPropertyType); $propertyMetadata = new PropertyMetadataEvent( mapperMetadata: $event->mapperMetadata, diff --git a/tests/AutoMapperMapToTest.php b/tests/AutoMapperMapToTest.php index 71afd750..644c3ef0 100644 --- a/tests/AutoMapperMapToTest.php +++ b/tests/AutoMapperMapToTest.php @@ -61,6 +61,7 @@ public function testMapToArray() $this->assertSame('transformed', $bar['transformFromExpressionLanguage']); $this->assertSame('bar', $bar['transformWithExpressionFunction']); $this->assertSame(0, $bar['fooInt']); + $this->assertSame(0.0, $bar['fooFloat']); $foo = new FooMapTo('bar'); $bar = $this->autoMapper->map($foo, 'array'); @@ -80,6 +81,7 @@ public function testMapToArray() $this->assertSame('bar', $bar['transformFromCustomTransformerService']); $this->assertSame('not transformed', $bar['transformFromExpressionLanguage']); $this->assertSame(0, $bar['fooInt']); + $this->assertSame(0.0, $bar['fooFloat']); } public function testMapToArrayGroups() diff --git a/tests/Fixtures/MapTo/FooMapTo.php b/tests/Fixtures/MapTo/FooMapTo.php index eb5a6a0d..7d89b6ca 100644 --- a/tests/Fixtures/MapTo/FooMapTo.php +++ b/tests/Fixtures/MapTo/FooMapTo.php @@ -22,7 +22,8 @@ public function __construct( #[MapTo('array', property: 'transformFromCustomTransformerService', transformer: TransformerWithDependency::class)] #[MapTo('array', property: 'transformFromExpressionLanguage', transformer: "source.foo === 'foo' ? 'transformed' : 'not transformed'")] #[MapTo('array', property: 'foo')] - #[MapTo('array', property: 'fooInt', targetType: new Type\BuiltinType(TypeIdentifier::INT))] + #[MapTo('array', property: 'fooInt', targetPropertyType: new Type\BuiltinType(TypeIdentifier::INT))] + #[MapTo('array', property: 'fooFloat', targetPropertyType: 'float')] public string $foo, ) { }