mark-gerarts / automapper-plus

An AutoMapper for PHP
MIT License
551 stars 30 forks source link

Nested mapping with array as a source #34

Closed VitaliiIsaenko closed 5 years ago

VitaliiIsaenko commented 5 years ago

Hi! I have a problem with nested object filling with array as a source. Is it possible with AutoMapper? Here is what I'd like to do, I attach a sample code. Could you have a look, please?

class Test
{
    public function test() {
        $mapper = $this->getParentClassMapper();

        $source = ["id" => 1, "name" => "Super name"];

        $parentObject = $mapper->map($source, ParentClass::class);
        var_dump($parentObject); die;
    }

    private function getParentClassMapper() {
        $config = new AutoMapperConfig();
        $config->registerMapping(DataType::ARRAY, ChildClass::class);
        $config->registerMapping(DataType::ARRAY, ParentClass::class)
            ->forMember('child', Operation::mapTo(ChildClass::class));
        return new AutoMapper($config);
    }
}

class ParentClass
{
    private $id;
    private $child;
}

class ChildClass
{
    private $name;
}

I know that the example looks strange and therefore give you some context. I have an address table in my database. It has fields like countryId and cityId. I don't want to get the city and country with separate queries and write an sql with joins to get all info. My sql looks like this (you can also see the tables and fields there):

SELECT address.id,
                       address.countryId,
                       address.stateId,
                       address.cityId,
                       address.street,
                       address.house,
                       address.zipCode,
                       address.longitude,
                       address.latitude,
                       address_country.nameEn as countryNameEn,
                       address_country.nameDe as countryNameDe,
                       address_country.region as countryRegion,
                       address_country.code as countryCode,
                       address_state.nameEn as stateNameEn,
                       address_state.nameDe as stateNameDe,
                       address_state.countryId as stateCountryId,
                       address_city.nameEn as cityNameEn,
                       address_city.nameDe as cityNameDe,
                       address_city.countryId as cityCountryId,
                       address_city.stateId as cityStateId

                FROM address
                JOIN address_country on (address_country.id = address.countryId)
                JOIN address_state on (address_state.id = address.stateId)
                JOIN address_city on (address_city.id = address.cityId)

                WHERE address.id = ?;

My Address model is the following:

/**
     * @var $id int
     */
    private $id;

    /**
     * @var $country Country|null
     */
    private $country;

    /**
     * @var $state State|null
     */
    private $state;

    /**
     * @var $city City|null
     */
    private $city;

    /**
     * @var $street string|null
     */
    private $street;

    /**
     * @var $house string|null
     */
    private $house;

    /**
     * @var $zipCode string|null
     */
    private $zipCode;

    /**
     * @var $longitude string|null
     */
    private $longitude;

    /**
     * @var $latitude string|null
     */
    private $latitude;

    /**
     * @var $countryId int|null
     */
    private $countryId;

    /**
     * @var $stateId int|null
     */
    private $stateId;

    /**
     * @var $cityId int|null
     */
    private $cityId;

As you can see, I would like to have city and country fields that are City and Country types. I would like to map the fields like cityNameEn to field nameEn of child city. Is that possible?
Thanks a lot for your effort, the library is awesomely useful!

mark-gerarts commented 5 years ago

Hey, are you in control of how your source data looks? The $source data doesn't resemble the structure of the parent-child relation you are trying to map. This makes the mapping a bit difficult. I'd expect the input data to look like this:

$source = [
    "id" => 1,
    'child' => ["name" => "Super name"]
];

This way you could just define your mapping like this:

$config = new AutoMapperConfig();
$config->registerMapping(DataType::ARRAY, ChildClass::class);
$config->registerMapping(DataType::ARRAY, ParentClass::class)
    ->forMember('child', Operation::mapTo(ChildClass::class, true));

(You need the true in your mapTo, see the comment just below here in the docs).

So if you can change the source structure, that would be the easiest solution. Otherwise you can try the following config. This is assuming your child class has a setter for the name property, or maybe a constructor:

$config->registerMapping(DataType::ARRAY, ParentClass::class)
    ->forMember('child', function ($source) {
        // Just manually map the name string to an instance of the child class.
        $child = new ChildClass();
        $child->setName($source['name']);
        return $child;
    });

If your child class doesn't provide any way to set the private name property, you can abuse the fact that the mapper has no problems with setting private properties. The mapper itself is passed as the second parameter of the mapping function, so you could construct an array and map that, basically recreating the first solution on the fly. Something like this:

$config->registerMapping(DataType::ARRAY, ParentClass::class)
    ->forMember('child', function ($source, AutoMapperInterface $mapper) {
        $child = ['name' => $source['name']];
        return $mapper->map($child, ChildClass::class);
    });

Let me know if this helped you, or if you have any other problems!

VitaliiIsaenko commented 5 years ago

That's an amazing explanation. I was thinking about these ways and you are right that they are perfectly valid and my example is too unintuitive.
Thank you again for the explanation!

fd6130 commented 3 years ago

@mark-gerarts Sorry for bump this old issue. For the solution above it is great for mapping a new object for the destination class.

How about mapping an existing entity object to destination class?

$source = [
    'child' => ["id" => "1"]
];
$config->registerMapping(DataType::ARRAY, ParentClass::class)
    ->forMember('child', function ($source) {
         // fetch entity from doctrine by "id" and return the child?
        return $child;
    });
mark-gerarts commented 3 years ago

No problem! You should be able to just fetch your entity from the repository where you indicate it. Unless I'm missing something, then please explain the issue some more.

The code could look something like this:

$config->registerMapping(DataType::ARRAY, ParentClass::class)
    ->forMember('child', function ($source) use ($repository) {
        return $repository->find($source['child']['id']);
    });