DX initiative
Hey,
The objective is to make AutoMapper easier to work with for developpers. In this issue I'll try to list all stuff developpers needs to fit their needs when using AutoMapper.
All the following stuff are examples and subject to change in their implementation.
- [x] Being able to give a custom property output for a given target
class Foo
{
public function __construct(
#[MapTo(('array', 'phrase')]
#[MapTo(Bar::class, 'phrase')]
public string $sentence,
) {
}
}
- [x] Being able to give a custom property input for a given target
class Foo
{
public function __construct(
#[MapFrom(('array', 'phrase')]
#[MapFrom(Bar::class, 'phrase')]
public string $sentence,
) {
}
}
- [x] Being able to map a field only if a certain condition is filled for a given target
class Foo
{
public function __construct(
#[MapIf(('array', fn() => true)]
#[MapIf(Bar::class, fn() => false)]
public string $sentence,
) {
}
}
- [x] Having a way to override / custom the mapping of a property for a given transformation
final readonly class FromSourceCustomPropertyTransformer implements CustomPropertyTransformerInterface
{
public function supports(string $source, string $target, string $propertyName): bool
{
return $source === UserDTO::class && $target === 'array' && $propertyName === 'name';
}
public function transform(mixed $input): mixed
{
return "{$input} set by custom property transformer";
}
}
- [ ] Having a way to override / custom the mapping for a given transformation as a whole
final readonly class FromTargetCustomModelTransformer implements CustomModelTransformerInterface
{
public function supports(string $source, string $target): bool
{
return $source === 'array' && $target === AddressDTO::class;
}
public function transform(mixed $input): mixed
{
$addressDTO = new \AutoMapper\Tests\Fixtures\AddressDTO();
$addressDTO->city = "{$input['city']} from custom model transformer";
return $addressDTO;
}
}
- [ ] Having a way to add custom code during some steps of the transformation:
beforeInitializationafterInitializationafterHydratation
class Foo
{
public function __construct(
public string $sentence,
) {
}
#[MapEvent(Event::AFTER_INITIALIZATION)]
public function doctrineHook(mixed $object): void
{
// link my entity to Doctrine UOW
}
}
hi!
- about
MapTo/MapFrom, I'm wondering if I wouldn't have chosen to pass an associative array:
MapTo(['array' => 'phrase', Bar::class => 'phrase'])
-
about
MapIf, be careful we cannot pass an arrow function in an attribute (but you can pass a static callable such as[self::class, 'method]) -
an event system could be nice, but I'm not sure to understand the example mentioning doctrine? what could be done with this?
-
I'm wondering if for
CustomModelTransformerInterfacewe should not introduce a concept likeNormalizerAwarein order to only fill SOME fields a let the rest be filled automatically
if I recall Ryan's message:
A) User.plainPassword -> maps to -> User.password but we need to go through the password hasher B) Same as above, but only map if User.plainPassword is not null (don’t try to hash an empty string and override the existing hashed password) C) Mapping A -> B, but B has some extra property on it that depends on, let’s say… 3 different properties on A and needs a service to calculate the final value. D) Mapping from UserDto -> User and because this is a PATCH operation / edit page, UserDto.id=5 and so we should actually query the database for User where id=5 instead of creating a new instance. E) Same as above, but with a relation: ProductDto.category is a CategoryDto and ProductDto.category.id=9. So when mapping ProductDto.category -> Product.category, the Category entity will be loaded from somewhere else (e..g database) but not instantiated.
A) I think this could be done with a CustomPropertyTransformerInterface as a service (we'd need DI to access PasswordHasher)
But may we introduce some nicer syntax?
transformer could be:
- an object:
trasnformer: new MyPasswordEncoder()is a valid syntax - a string, and we'd grab a service from the DI
if method is omitted, the transformer needs to be callable.
We could even be possible to pass transform: [self::class, 'transformer'] to keep the transformation in the same class.
class UserDto
{
public function __construct(
#[MapTo(target: UserEntity::class, field; 'password', transformer: MyPasswordEncoder::class, method: 'encodePassword')]
public string $plainPassword,
) {
}
}
B) This makes me think that the MapIf() you mentioned does not clearly the "direction" of the mapping:
does #[MapIf(Bar::class, fn() => false)] is a condition from Bar::class or to Bar::class?
couldn't we add a condition property to #[MapTo] and #[MapFrom]? this would remove the ambiguity
C) I think the C could leverage flattening. We'd still need to think about a nice syntax for this.
I'm wondering if the C is not complex enough to use CustomModelTransformerInterface (he says we need a service to compute a field from three other ones)
I think D&E are more related to Api-Platform
Most of comments were addressed, only remaining point is if we want an user to fully replace a Mapper for a specific source and target, i'm not sure since you can tell how to instantiate (with provider) and which property to map (with attributes and transformers) this roughly covers most use cases.
We can close this if we don't want it for the moment (i think there is already enough features for a 9.0 not need to add more)
Thanks a lot for all the work @joelwurtz <3 much appreciated, I think we can close this issue now.
As this issue is pinned, I'm suggesting a new DX improvement here:
- [ ] Using MapTo or MapFrom with invalid "properties" is ignored, it should throw an exception
What do you consider "invalid" properties ? Any example to share ?
For example with this attribute, no errors are thrown, the generate code is just empty.
#[MapTo(target: UserEntity::class, field; 'covfefe')]
But it should be possible for AutoMapper to know there are no "covfefe" on UserEntity.