AN AUTOMAPPER FOR PHP, THE POWERFUL AND SIMPLE SOLUTION FOR MAPPINGS

Did you wonder how to eliminate problems with complexity of mapping, whose are always coming back to you?

It concerns a many developers, who are creating backend-layer of web applications. This applies in particular to building an APIs, using Event-Driven architecture with a lot of DTO / VO objects. If you also are using the DDD (domain-driven design) approach, you have to keep the entities inside the domain layer and do not expose them outside the domain to make sure, that they will be consistent and the outer things like application layer will not directly affect theirs internal states.

×

By „complexity of mapping and serializations” I mean all things like complex objects, domain entities, etc. whose you want to serialize or combine its properties in many ways to create another objects with different structures, whose will fit best to business logic needs.

SERIALIZERS

If you’re using PHP and Symfony framework you probably use one of Serializer implementations like JMSSerializer bundle.

For example: you have an User entity, which also contains password, so if you want to serialize it to JSON as a API response – you have to hide the „password” field to be not serialized and exposed to the outside. It’s crucial and very important thing among others in terms of security. One way to archive that is putting an JMS annotations (like “Exclude“) into the Entity above the fields, whose you want to hide (see the exclusion strategies from docs). As you probably know it works:

  1. use Doctrine\ORM\Mapping as ORM;
  2. use JMS\Serializer\Annotation as JMS;
  3. /** @ORM\Entity */
  4. class User
  5. {
  6.     /**
  7.      * @ORM\Id
  8.      * @ORM\Column(type=»integer»)
  9.      * @ORM\GeneratedValue(strategy=»AUTO»)
  10.      */
  11.     private $id;
  12.     /**
  13.      * @ORM\Column(name=»password», type=»string»)
  14.      * @JMS\Exclude()
  15.      */
  16.     private $password;
  17.    //…
  18. }
WHEN IT BECAME MORE COMPLEX…

Let’s go a step further and suppose, that we have much more fields to hide, because of personal data protection. e.g. email address, phone number, birth date and so on.

It’s also easy way to do that as in previous example, but it will become more complicated, when we also want to provide (and not hide) this personal data to theirs owner, which may want to edit personal data in the form.

Now we’ve got an two different sets of fields:

  • the full one (for data owner)
  • the limited one (with only public informations for others)

JMS Serializer have also solution for that – it’s called „groups”, which is a pack of rules, annotations, which will allow or deny the serialization of specific fields depends of the “group“.

You can choose multiple combinations of object fields to be represented as different groups. Through serialization process only fields from choosen group will be exposed to the outside.

php json jms serializer

CONCLUSIONS

Now it’s time to show where and what the problem is. All these solutions above are designed only for serialization process, so this will be done mainly when the controller returns a response. It is not enough. I want you to know, that:

  • it does not help with denying access and preventing manipulations on models/domain entities, which is crucial for theirs cohesion. In addition the immutability of some objects (like events) cannot be respected
  • you can only choose the fields, that you want to hide, but you cannot change theirs original names or internal structure
  • as you have more and more variants of field sets, you code will consist of huge amount of annotations, which might affect lower readability and misunderstanding the structure of grouped fields for serializer. It also isn’t a good thing in terms of clean code.
  • you can’t add additional extra fields to the serialized object using only JMS (which might be for example an counters of user’s comments, blog posts and so on)

If you really want to do that you have to handle it by yourself. Probably they will be converters, factories, whose will map original object to the object with extra fields. Creating such solutions is a tedious, tiresome task and prone to errors. You probably know – it happens sometimes.

The good news is that we have already way to resolve this case simply, clearly and with a short amount of code. Thanks that you can forgot about using JMS annotations, serializer rules. Spending a lot of time on building your own converters for each type, whose increase your complexity and decrease readability of source code isn’t a dream job.

IT’S TIME TO MEET AN AUTOMAPPER

You may not heard about it before, as I do, and probably most of PHP Symfony developers around the world. Because AutoMapper it’s a powerful library mainly known as a standard since couple of years by .NET developers.

While creating one project in .NET Core 2.0 framework I had thoughs about: „why PHP developers don’t use and even don’t know AutoMapper?” So I decided to make some research and I found an PHP library called AutoMapper+, which was written by Mark Gerarts with other contributors and inspired by the equivalent .NET AutoMapper.

Due to PHP language limitations, this library was written with similiar functionalities like in a C# version. AutoMapperPlus have also an wrapper to give Symfony framework developers ability to use it as bundle.

Take a look at those GitHub repositories:

THE POSSIBILITIES OF AUTOMAPPER+

AutoMapper+ allows you to convert objects in various ways to any other objects (or even collections of objects), whose can represent objects made to measure for your needs.

It’s an universal factory, with its own customizable configuration. You can just add new mapping template to configuration once (yes, only once) and then use this mappings everywhere whenether you want. You just need to us an instance of AutoMapperInterface, which will handle convertions for you.

Fields inside mapped objects can also be converted by other mappings. So you have an opportunity to choose which mapping will convert a choosen field (remember it isn’t necessary – it’s only a option).

Obviously you can make an calculations, operations based by original object fields and produce the values for new extra fields with a result of these calculations.

Private fields

During the conversion process also private fields of object will be converted, so you don’t need to care about changing them to public in the original object. Thanks that you will not break the encapsulation (basic paradigm of object-oriented programming).

AUTOMATICALLY MAPPINGS FOR THE IDENTICAL FIELD NAMES

There is one more thing you should know:  if field names of original object and destination object will be the same, the AutoMapper+ will automatically copy the values between them, so you don’t have to attach them in the configuration.

But when the names will be different, then you should tell the AutoMapper+ what you want to do with them. All these stuff can be done through “mapping configuration” classes.

EXAMPLE WITH SYMFONY BUNDLE

Let’s assume, that we have some students in a School. From time to time each student have an exam to write. The exams might be failed or passed.

As a developer you’re making the web application, which should give those students possibility to see a list of all theirs exams and amount of passed exams.

Other students should see a list of students in their class, but only with public data like “amount of student exams” in general.

In this example we want convert User Model (which represent a specific student) to DTO, which will be sent to the GUI. Because we have two different sets of data: public and private only for  owners, we must create two different DTO’s.

Of course we can use an AutoMapper+. Conversion can be made for example at Service layer, so we will avoid possible domain model manipulations in the controllers.

automapper plus dto

SERVICE LAYER

To archive conversions you should inject implementation of AutoMapperInterface into your Service class. Then you will be able to use AutoMapper methods to:

  • map one object map($sourceObject, $destinationClassName)
  • map collections of objects mapMultiple(array $collection, $destinationClassName)
  1. use AutoMapperPlus\AutoMapperInterface;
  2.  
  3. class UserProvider
  4. {
  5.     /** @var UserRepositoryInterface */
  6.     private $userRespository;
  7.     /** @var AutoMapperInterface */
  8.     private $autoMapper;
  9.     /**
  10.      * @param UserRepositoryInterface $userRespository
  11.      * @param AutoMapperInterface $autoMapper
  12.      */
  13.     public function __construct(
  14.         UserRepositoryInterface $userRespository,
  15.         AutoMapperInterface $autoMapper
  16.     ){
  17.         $this>userRespository = $userRespository;
  18.         $this>autoMapper = $autoMapper;
  19.     }
  20.     /**
  21.      * @param UsersFromSchoolQuery $query
  22.      * @return PublicUserDto[]|array
  23.      */
  24.     public function getUsersFromSchool(UsersFromSchoolQuery $query) : array
  25.     {
  26.         /** @var User[]|array */
  27.         $users = $this>userRespository>getAllFromSchool(
  28.             $query>getSchool(),
  29.             $query>getLimit(),
  30.             $query>getOffset()
  31.         );
  32.         return $this>autoMapper>mapMultiple($users, PublicUserDto::class);
  33.     }
  34.     /**
  35.      * @param Uuid $userId
  36.      * @return PrivateUserDto
  37.      */
  38.     public function getPrivateUserData(Uuid $userId) : PrivateUserDto
  39.     {
  40.         /** @var User */
  41.         $user = $this>userRespository>getOneById($userId);
  42.         return $this>autoMapper>map($user, PrivateUserDto::class);
  43.     }
  44. }
MAPPING CONFIGURATION

To let AutoMapper+ know about our conversion from User model to the DTO’s, we have to create mapping templates. These mappings should be added to a new configuration class. Don’t worry, you have to do that only once. Then you can use these mappings wherever you want.

  1. use AutoMapperPlus\AutoMapperPlusBundle\AutoMapperConfiguratorInterface;
  2. use AutoMapperPlus\Configuration\AutoMapperConfigInterface;
  3. class MapperConfig implements AutoMapperConfiguratorInterface
  4. {
  5.     public function configure(AutoMapperConfigInterface $config): void
  6.     {
  7.         $config>registerMapping(Exam::class, ExamDto::class);
  8.         $config>registerMapping(User::class, PublicUserDto::class)
  9.             >forMember(‘examsAmount’, function (User $user) {
  10.                 return $user>getExams()>count();
  11.             });
  12.         $config>registerMapping(User::class, PrivateUserDto::class)
  13.             >forMember(‘exams’, Operation::mapTo(ExamDto::class))
  14.             >forMember(‘passedExamsAmount’, function (User $user) {
  15.                 $exams = $user>getExams();
  16.                 $passedExams = $exams>filter(
  17.                     function(Exam $exam) {
  18.                         return $exam>isPassed();
  19.                     }
  20.                 );
  21.                 return $passedExams>count();
  22.             });
  23.     }
  24. }

Then we can register this class as mapping configuration in e.g. “services.yml” file

  1. my_automapper_config:
  2.     class: App\Configuration\MapperConfig
  3.     tags: [‘automapper_plus.configurator’]

THE MAPPING CONFIGURATION IMPROVEMENTS

Only one mapping configuration class will become bigger and bigger when we will constantly add new mappings. As you know this approach isn’t good in terms of clean code.

DIVIDE THE CONFIGURATION CLASS INTO MANY SMALLER ONES

You can register many mapper configuration classes. To avoid situation described above – you can create multiple classes to handle mappings divided into some categories. For example all mappings related with User entity may be putted to UserMapperConfig class, so for Exam entity this should be ExamMapperConfig class etc.

USE A CUSTOM MAPPER CLASS INSTEAD OF USING CLOSURES – FOR COMPLEX CASES

When mapping require plenty of custom operations and calculations it would be better to not put that directly into closure in configuration class.

For obvious reasons this complex code does contain part of business logic, so you may also want to write some unit tests for that. Luckily for us the Automapper+ give us possibility to use custom mapper classes:

  1. class ExamMapper extends CustomMapper
  2. {
  3.     /**
  4.      * @param Exam $exam
  5.      * @param ExamDto $dto
  6.      * @return ExamDto
  7.      */
  8.     public function mapToObject($exam, $dto)
  9.     {
  10.         $dto>id = $exam>getId();
  11.         $dto>passed = $exam>isPassed();
  12.         $dto>description = $exam>getDescription();
  13.         return $dto;
  14.     }
  15. }
  16. // …in your configuration class
  17. $config
  18.     >registerMapping(Exam::class, ExamDto::class)
  19.     >useCustomMapper(new ExamMapper());

As you can see in this case writing unit tests for ExamMapper will be a trivial task, but it is just an example.

These approaches will definitely helps to keep a single responsibility principle and high readability in configuration classes.

SIMILAR LIBRARIES FOR OTHER PROGRAMMING LANGUAGES

Of course this article is focused on PHP version of AutoMapper. If you are a developer using other programming languages – maybe you would be interested in equivalent libraries:

SHORT SUMMARY

In comparison to building new structures during the serialization process only, we can now do this in other places. Finally, serialization of ready-made DTO / VO objects will not require additional operations and annotations.

AutoMapper can also be often used in places where we convert an domain model into the event object. Later we can send such an event to message queue (like RabbitMQ).

We can also add mapping in both-directions called “reverse map“. It is useful if we’re using CQRS and we want to convert command object to a domain model without rewriting all fields manually.

Of course library like AutoMapper isn’t an perfect solution for every application, but it definitely reduces the amount of code and work associated with mapping. Remember to follow the SOLID principles, and trust your own mind. Don’t stop discovering another ways to solve object-oriented problems – maybe you’ll find something better?

There are also other functionalities, about whose I didn’t wrote, so if you want to learn more deeply the AutoMapper+, please take a look at documentation. Good luck!