Buy Access to Course
25.

MicroMapper: Central DTO Mapping

|

Share this awesome video!

|

Keep on Learning!

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Doing the data transformation, from UserApi to the User entity, or the User entity to UserApi, is the only part of our provider and processor that isn't generic and reusable. Rats! If it wasn't for that code, we could create a DragonTreasureApi class and do this whole thing over again with, like almost no work! Fortunately, this is a well-known problem called "data mapping".

For this tutorial, I tried a few data mapping libraries, most notably jane-php/automapper-bundle, which is super-fast, advanced, and fun to use. However, it isn't quite as flexible as I needed... and extending it looked complex. Honestly... I got stuck in a few places... though I know that work is being done to make this package even friendlier.

The point is, we're not going to use that library. Instead, to handle the mapping, I created a small package of my own. It's easy to understand, and gives us full control... even if it's not quite as cool as jane's automapper.

Installing micro-mapper

So let's get it installed! Run:

composer require symfonycasts/micro-mapper

That kind of sounds like a superhero. Now that we have this in our app, we have one new micromapper service that's good at converting data from one object to another. Let's start by using it in our processor.

Using the MicroMapper Service

Up on top, autowire a private MicroMapperInterface $microMapper.

53 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 14
use Symfonycasts\MicroMapper\MicroMapperInterface;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function __construct(
// ... lines 20 - 23
private MicroMapperInterface $microMapper
)
{
}
// ... lines 29 - 51
}

And down here, for all the mapping stuff, copy the existing logic, because we'll need it in a minute. Replace it with return $this->microMapper->map(). This has two main arguments: The $from object, which will be $dto and the toClass, so User::class.

53 lines | src/State/EntityClassDtoStateProcessor.php
// ... lines 1 - 14
use Symfonycasts\MicroMapper\MicroMapperInterface;
class EntityClassDtoStateProcessor implements ProcessorInterface
{
public function __construct(
// ... lines 20 - 23
private MicroMapperInterface $microMapper
)
{
}
// ... lines 29 - 47
private function mapDtoToEntity(object $dto): object
{
return $this->microMapper->map($dto, User::class);
}
}

Done! Well... not quite, but let's try running testPostToCreateUser anyway.

symfony php bin/phpunit --filter=testPostToCreateUser

And... it fails with a 500 error. The interesting thing is what that 500 error says. Let's "View Page Source" so we can read this even better. It says

No mapper found for App\UserResource\UserApi -> App\Entity\User

And this comes from MicroMapper. This basically says:

Hey, I don't know how to convert a UserApi object to a User object! Halp!

Creating a Mapper

MicroMapper isn't magic... it's really the opposite. To teach micro mapper how to do this conversion, we need to create a class that explains what we want. That's called a mapper class. And these are fun!

Let me start by closing a few things... and then creating a new Mapper/ directory in src/. Inside of that, add a new PHP class called... how about UserApiToEntityMapper, because we're going from UserApi to the User entity.

This class needs 2 things. First, to implement MapperInterface.

24 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 2
namespace App\Mapper;
// ... lines 4 - 7
use Symfonycasts\MicroMapper\MapperInterface;
// ... lines 9 - 10
class UserApiToEntityMapper implements MapperInterface
{
// ... lines 13 - 22
}

And second, above the class, to describe what it's mapping to and from, we need an #[AsMapper()] attribute with from: UserApi::class and to: User::class.

24 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\UserApi;
use App\Entity\User;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: UserApi::class, to: User::class)]
class UserApiToEntityMapper implements MapperInterface
{
// ... lines 13 - 22
}

To help the interface, go to "Code Generate" (or "command" + "N" on a Mac) and generate the two methods it needs: load() and populate(). For starters, let's dd($from, $toClass).

24 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\UserApi;
use App\Entity\User;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: UserApi::class, to: User::class)]
class UserApiToEntityMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
dd($from, $toClass);
// TODO: Implement load() method.
}
public function populate(object $from, object $to, array $context): object
{
// TODO: Implement populate() method.
}
}

Now, just by creating this and giving it #[AsMapper], when we use MicroMapper to do this transformation, it should call our load() method. Let's see if it does!

Run the test:

symfony php bin/phpunit --filter=testPostToCreateUser

And... got it! There's the UserApi object we're passing, and it's passing us the User class. The purpose of load() is to load the $toClass object and return it, like by querying for a User entity or creating a new one.

To do the query, on top, add public function __construct() and inject the normal UserRepository $userRepository. Down here, this will hold the same code that we saw earlier. I like to say $dto = $from and assert($dto instanceof UserApi). That helps my brain and my editor.

Next, if our $dto has an id, then call $this->userRepository->find($dto->id). Else, create a brand new User() object.

38 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 6
use App\Repository\UserRepository;
// ... lines 8 - 11
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
private UserRepository $userRepository,
)
{
}
public function load(object $from, string $toClass, array $context): object
{
$dto = $from;
assert($dto instanceof UserApi);
$userEntity = $dto->id ? $this->userRepository->find($dto->id) : new User();
// ... lines 26 - 30
}
// ... lines 32 - 36
}

It's that simple. And if, for some reason, we don't have a $userEntity, throw new \Exception('User not found'), similar to what we did before. Down here, return $userEntity.

38 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 6
use App\Repository\UserRepository;
// ... lines 8 - 11
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
private UserRepository $userRepository,
)
{
}
public function load(object $from, string $toClass, array $context): object
{
$dto = $from;
assert($dto instanceof UserApi);
$userEntity = $dto->id ? $this->userRepository->find($dto->id) : new User();
if (!$userEntity) {
throw new \Exception('User not found');
}
return $userEntity;
}
// ... lines 32 - 36
}

So we've initialized our $to object and returned it. And that's the point of load(): to do the least amount of work to get the $to object... but without populating the data.

Internally, after calling load(), micro mapper will then call populate() and pass us the User entity object that we just returned. To see this, let's dd($from, $to).

38 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 6
use App\Repository\UserRepository;
// ... lines 8 - 11
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
private UserRepository $userRepository,
)
{
}
public function load(object $from, string $toClass, array $context): object
{
$dto = $from;
assert($dto instanceof UserApi);
$userEntity = $dto->id ? $this->userRepository->find($dto->id) : new User();
if (!$userEntity) {
throw new \Exception('User not found');
}
return $userEntity;
}
public function populate(object $from, object $to, array $context): object
{
dd($from, $to);
}
}

Run that test:

symfony php bin/phpunit --filter=testPostToCreateUser

Perfect! Here's our "from" UserApi object, and the new User entity.

Now... you might be wondering why we have both a load() method and a populate() method... when it seems like these could just be one method. And you'd mostly be right! But there's a technical reason why they're separated, and it'll come in handy later when we talk about relationships. But for now, you can imagine these two methods are really just one, continuous process: load() is called, then populate().

And no surprise, this is where we will take the data from the $from object and put it onto the $to object. Once again, to keep me sane, I'll say $dto = $from and assert($dto instanceof UserApi)... then $entity = $to and assert($entity instanceof User).

The code down here is going to be really boring... so I'll paste it. At the bottom, return $entity.

52 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 12
class UserApiToEntityMapper implements MapperInterface
{
// ... lines 15 - 34
public function populate(object $from, object $to, array $context): object
{
$dto = $from;
assert($dto instanceof UserApi);
$entity = $to;
assert($entity instanceof User);
$entity->setEmail($dto->email);
$entity->setUsername($dto->username);
if ($dto->password) {
$entity->setPassword($this->userPasswordHasher->hashPassword($entity, $dto->password));
}
// TODO dragonTreasures if we change them to writeable
return $entity;
}
}

We're using $this->userPasswordHasher here... so we also need to make sure, at the top, to add private UserPasswordHasherInterface $userPasswordHasher.

52 lines | src/Mapper/UserApiToEntityMapper.php
// ... lines 1 - 7
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
// ... lines 9 - 12
class UserApiToEntityMapper implements MapperInterface
{
public function __construct(
// ... line 16
private UserPasswordHasherInterface $userPasswordHasher,
)
{
}
// ... lines 21 - 34
public function populate(object $from, object $to, array $context): object
{
$dto = $from;
assert($dto instanceof UserApi);
$entity = $to;
assert($entity instanceof User);
$entity->setEmail($dto->email);
$entity->setUsername($dto->username);
if ($dto->password) {
$entity->setPassword($this->userPasswordHasher->hashPassword($entity, $dto->password));
}
// TODO dragonTreasures if we change them to writeable
return $entity;
}
}

So this is basically the same code we had before... but in a different spot.

Let's see what the test thinks!

symfony php bin/phpunit --filter=testPostToCreateUser

It passes! This is huge! We've offloaded this work to our mapper... which means our processor is almost completely generic. Now we can remove the UserPasswordHasher that we don't need anymore... and the UserRepository up here. We can even remove those use statements.

We still do need to write the mapping code, but now it lives in a nice, central location.

Mapping the Other Direction

Ready to repeat this for the provider. Close the processor... and open it up. This time, we're going from the User entity to UserApi. Copy all of this code, delete it and, just like before, autowire MicroMapperInterface $microMapper. Down here, this simplifies to return $this->microMapper->map() going from our $entity to UserApi::class.

60 lines | src/State/EntityToDtoStateProvider.php
// ... lines 1 - 11
use App\ApiResource\UserApi;
// ... line 13
use Symfonycasts\MicroMapper\MicroMapperInterface;
// ... line 15
class EntityToDtoStateProvider implements ProviderInterface
{
public function __construct(
// ... lines 19 - 20
private MicroMapperInterface $microMapper
)
{
}
// ... lines 26 - 54
private function mapEntityToDto(object $entity): object
{
return $this->microMapper->map($entity, UserApi::class);
}
}

Sweet! If we tried this now, we'd get a 500 error because we don't have a mapper for it. Back in src/Mapper/, create a new class called UserEntityToApiMapper... implement MapperInterface... and above the class, add #[AsMapper()]. In this case, we're going from: User::class, to: UserApi::class.

39 lines | src/Mapper/UserEntityToApiMapper.php
// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\UserApi;
use App\Entity\User;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: User::class, to: UserApi::class)]
class UserEntityToApiMapper implements MapperInterface
{
// ... lines 13 - 37
}

Implement both of the methods we need... and we start pretty much the same way as before, with $entity = $from and assert($entity instanceof User).

39 lines | src/Mapper/UserEntityToApiMapper.php
// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\UserApi;
use App\Entity\User;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: User::class, to: UserApi::class)]
class UserEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof User);
// ... lines 17 - 21
}
// ... lines 23 - 37
}

Down here, to create the DTO, we don't need to do any queries. We're always going to instantiate a fresh new UserApi(). Set the ID onto it with $dto->id = $entity->getId()... then return $dto.

39 lines | src/Mapper/UserEntityToApiMapper.php
// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\UserApi;
use App\Entity\User;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: User::class, to: UserApi::class)]
class UserEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof User);
$dto = new UserApi();
$dto->id = $entity->getId();
return $dto;
}
// ... lines 23 - 37
}

Ok, the job of the load() method is really to create the $to object and... at least make sure it has its identifier if there is one.

Everything else we need to do is down here in populate(). Start our usual way: $entity = $from, $dto = $to and two asserts: assert($entity instanceof User) and assert($dto instanceof UserApi). Below that, use the exact code we had before. We're just transferring the data. At the bottom, return $dto.

39 lines | src/Mapper/UserEntityToApiMapper.php
// ... lines 1 - 2
namespace App\Mapper;
use App\ApiResource\UserApi;
use App\Entity\User;
use Symfonycasts\MicroMapper\AsMapper;
use Symfonycasts\MicroMapper\MapperInterface;
#[AsMapper(from: User::class, to: UserApi::class)]
class UserEntityToApiMapper implements MapperInterface
{
public function load(object $from, string $toClass, array $context): object
{
$entity = $from;
assert($entity instanceof User);
$dto = new UserApi();
$dto->id = $entity->getId();
return $dto;
}
public function populate(object $from, object $to, array $context): object
{
$entity = $from;
$dto = $to;
assert($entity instanceof User);
assert($dto instanceof UserApi);
$dto->email = $entity->getEmail();
$dto->username = $entity->getUsername();
$dto->dragonTreasures = $entity->getPublishedDragonTreasures()->getValues();
$dto->flameThrowingDistance = rand(1, 100);
return $dto;
}
}

Phew! Let's try this! Head over to your browser, refresh this page, and... oh...

Full authentication is required to access this resource.

Of course. That's because we added security! Head back over to the homepage, click this username and password shortcut... boop... and now try to refresh that page. It works! We are missing some of the data, though, which is my fault.

I said $dto = new UserApi(). So instead of modifying the $to object I'm being passed, I created a new one... and the original wasn't modified. There we go. If I try it again... much better.

So this is huge people! Our provider and processor are now generic! Let's finish the process of making them work for any API resource class next