In many Symfony applications it's common to use DTO objects to carry data between layers. For example, you might create a DTO from a Doctrine entity, modify it using a Symfony Form, and then map the updated data back to the entity.
The code to map the DTO back into an entity often looks like this:
1 2 3 4 5
$user = new User();
$user->name = $dto->name;
$user->email = $dto->email;
$user->roles = ['ROLE_USER'];
// ...
This is tedious and error-prone. That's why Symfony 7.3 introduces a new ObjectMapper component to transfer data between objects. With it, the previous example becomes much simpler:
1 2 3 4 5
// when creating a new object from another one
$user = $mapper->map($dto, User::class);
// when updating an existing object with another one
$mapper->map($dto, $user);
In this example, $mapper
is the main object mapper service, automatically
autowired in controllers and services when type-hinting for
Symfony\Component\ObjectMapper\ObjectMapperInterface
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// src/Controller/UserController.php
namespace App\Controller;
use App\Dto\UserInput;
use App\Entity\User;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
class UserController extends AbstractController
{
public function updateUser(UserInput $userInput, ObjectMapperInterface $objectMapper): Response
{
$user = new User();
// map properties from UserInput to User
$objectMapper->map($userInput, $user);
// ... persist $user and return response
return new Response('User updated!');
}
}
ObjectMapper uses the PropertyAccess component to set or update the values
of properties in the target object. You can also use the #[Map]
attribute
to configure mapping behavior explicitly:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
// src/Dto/UserDto.php
namespace App\Dto;
use App\Entity\User;
use Symfony\Component\ObjectMapper\Attribute\Map;
#[Map(target: User::class)]
class UserDto
{
// 'target' refers to the name of this property in the target object
#[Map(target: 'emailAddress')]
public string $email = '';
// 'if' allows to define expressions evaluated at runtime;
// if the result is TRUE, the property is mapped; otherwise it's ignored
#[Map(if: false)]
public string $debugOnly = '';
// 'transform' applies a PHP function or callback before mapping the value
#[Map(transform: 'strtolower')]
public string $username = '';
}
This new component is marked as an experimental feature in Symfony 7.3, so it may evolve, but it's already powerful, flexible, and ready for many use cases.
For example, you can use the #[TargetClass]
condition to map a property only
when targeting a specific class. In this example, the User
class can be mapped
to two different DTOs, but the $lastLoginIp
property should only be copied to
one of them:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use App\Dto\AdminUserProfile;
use App\Dto\PublicUserProfile;
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Condition\TargetClass;
#[Map(target: PublicUserProfile::class)]
#[Map(target: AdminUserProfile::class)]
class User
{
#[Map(target: 'ipAddress', if: new TargetClass(AdminUserProfile::class))]
public ?string $lastLoginIp = '192.168.1.100';
// ...
}
The ObjectMapper component also includes other advanced features:
- Use services to define complex mapping conditions or value transformations;
- Map based on the target class, not the source;
- Automatically handle recursion and cyclic object graphs;
- Write your own mapping logic (similar to Java's MapStruct);
- Conditionally map to different targets based on methods or context.
This component makes it easier to work with DTOs and helps decouple API input/output from your domain model. It's also great for legacy projects where new DTOs must integrate with existing complex objects. It reduces manual mapping code, prevents bugs, and keeps your architecture clean.
Oh god I have so many of those mapping classes in my codebases... Using this new component will save me thousands of LOC, and maybe also make the apps faster. Thank you very much for this one!