jolicode / automapper

:rocket: Very FAST :rocket: PHP AutoMapper with on the fly code generation
https://automapper.jolicode.com/
MIT License
154 stars 15 forks source link

Map from flat datas to structured datas #201

Open dsoriano opened 3 weeks ago

dsoriano commented 3 weeks ago

Hi all,

This is my use case :

class Source
{
    #[MapTo(target: 'Target', groups: ['write'])]
    public string $name;

    public string $street;
    public string $zip;
    public string $city;
    public string $country;
}

class TargetAddress {
    public string $street;
    public string $zip;
    public string $city;
    public string $country;
}

class Target
{
    public string $name;
    public ?TargetAddress $address = null;
}

$target = $automapper->map($source, new Target(), ['groups' => ['write']]);

I have to map the address fields from Source into an object in Target. There is a limitation : the target class can't be modified beause it comes from an external lib.

First I thought to do that :

class Source
{
    #[MapTo(target: 'Target', groups: ['write'])]
    public string $name;

    public string $street;
    public string $zip;
    public string $city;
    public string $country;

    #[MapTo(target: 'Target', property: 'address', groups: ['write'])]
    public function getAddress(): array
    {
        return [
            'street' => $this->street,
            'zip' => $this->zip,
            'city' => $this->city,
            'country' => $this->country,
        ];
    }
}

But I doesn't work, It can convert the array to a new TargetAddress instance.

Then I tried :

class Source
{
    #[MapTo(target: 'Target', groups: ['write'])]
    public string $name;

    public string $street;
    public string $zip;
    public string $city;
    public string $country;

    #[MapTo(target: 'Target', property: 'address', groups: ['write'])]
    public function getAddress(): \stdClass
    {
        return (object)[
            'street' => $this->street,
            'zip' => $this->zip,
            'city' => $this->city,
            'country' => $this->country,
        ];
    }
}

Here the TargetAddress is created, but because of the group, the properties are not mapped.

Is there a way to do that simply ?

Thanks in advance

Korbeil commented 3 weeks ago

Hey @dsoriano and thanks for using AutoMapper !

After some tests, you should use the transformer on a class attribute as following:

#[MapTo(target: Target::class, property: 'address', transformer: 'getAddress', groups: ['write'])]
class Source
{
    #[MapTo(target: Target::class, groups: ['write'])]
    public string $name;

    public string $street;
    public string $zip;
    public string $city;
    public string $country;

    public function getAddress(): TargetAddress
    {
        $targetAddress = new TargetAddress();
        $targetAddress->street = $this->street;
        $targetAddress->zip = $this->zip;
        $targetAddress->city = $this->city;
        $targetAddress->country = $this->country;

        return $targetAddress;
    }
}

To explain a bit: MapTo attributes can only be placed onto properties or onto the class, so in your examples the MapTo attribute on getAddress was being ignored :wink: You need to make it a class attribute that use the method as transformer so it works well :ok_hand:

This should fix your issue ! Tell me if you have anything else you wanna ask otherwise feel free to close the issue. Regards, Baptiste

dsoriano commented 3 weeks ago

Hi @Korbeil Thanks for your answer.

I'm pretty sure that the attribute MapTo on the method is not the problem and works. This code is working well :

class Source
{
    #[MapTo(target: 'Target')]
    public string $name;

    public string $street;
    public string $zip;
    public string $city;
    public string $country;

    #[MapTo(target: 'Target', property: 'address')]
    public function getAddress(): \stdClass
    {
        return (object)[
            'street' => $this->street,
            'zip' => $this->zip,
            'city' => $this->city,
            'country' => $this->country,
        ];
    }
}

class TargetAddress {
    public string $street;
    public string $zip;
    public string $city;
    public string $country;
}

class Target
{
    public string $name;
    public ?TargetAddress $address = null;
}

$automapper = AutoMapper::create();

$source = new Source();
$source->name = 'John Doe';
$source->street = 'Main Street 123';
$source->zip = '12345';
$source->city = 'Springfield';
$source->country = 'USA';

$target = $automapper->map($source, new Target());

The dump : 2024-10-27 11_36_33-Firefox Developer Edition

The problem is coming when I add the serialization group. Event this code doesn't work :

    #[MapTo(target: 'Target', property: 'address', groups: ['write'])]
    public function getAddress(): TargetAddress
    {
        $address = new TargetAddress();
        $address->street = $this->street;
        $address->zip = $this->zip;
        $address->city = $this->city;
        $address->country = $this->country;
        return $address;
    }

Before your suggestion, the only workaround I found is to create another class SourceAddress like this:

class SourceAddress {
    #[MapTo(target: 'TargetAddress', groups: ['write'])]
    public string $street;

    #[MapTo(target: 'TargetAddress', groups: ['write'])]
    public string $zip;

    #[MapTo(target: 'TargetAddress', groups: ['write'])]
    public string $city;

    #[MapTo(target: 'TargetAddress', groups: ['write'])]
    public string $country;
}

And my method getAddress :

    #[MapTo(target: 'Target', property: 'address', groups: ['write'])]
    public function getAddress(): SourceAddress
    {
        $address = new SourceAddress();
        $address->street = $this->street;
        $address->zip = $this->zip;
        $address->city = $this->city;
        $address->country = $this->country;
        return $address;
    }

It seems confirm that this is a serialization group problem. But by using a new SourceAddress class and populate it manually for the mapping, I lost a little bit the benefits of an automapping.

Your code is working well also, but the problem is the transformer must return a TargetAddress class. This is restrictive if I want to use the same transformer for another target, and here again I lots the benefits of automapping if I have to populate a part of the target class manually.

Thank you