The ObjectMapper component was introduced in Symfony 7.3 to eliminate the repetitive boilerplate involved in copying data between objects, such as DTOs and entities. Symfony 8.1 builds on that foundation with several improvements that make mappings more expressive and easier to debug.
Define Mappings on the Target Class
ObjectMapper typically reads the #[Map(target: ...)] attribute from the
source class. In hexagonal or clean architectures, you may prefer to keep domain
objects free of mapping metadata. Symfony 8.1 now lets you declare
#[Map(source: ...)] on the target class instead:
1 2 3 4 5 6 7 8 9 10 11
use App\Domain\Quote;
use Symfony\Component\ObjectMapper\Attribute\Map;
// the mapping lives on the view model, so the Quote domain
// object stays free of any mapping concern
#[Map(source: Quote::class)]
final class QuoteView
{
public string $id;
public int $amount;
}
Symfony automatically discovers these attributes and builds the class map, so no explicit target class is required when mapping:
1 2 3 4 5 6 7
public function show(Quote $quote, ObjectMapperInterface $mapper): Response
{
// Symfony knows Quote maps to QuoteView, so no target is needed
$view = $mapper->map($quote);
// ...
}
Skip Null Values With the IsNotNull Condition
When mapping a source object onto an existing target (for example, during a
partial "PATCH" update), null values in the source overwrite the target's
existing values. Symfony 8.1 adds the built-in IsNotNull condition so you
can skip a property whenever its source value is null:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Condition\IsNotNull;
class UserPatch
{
public function __construct(
#[Map(if: new IsNotNull())]
public ?string $name = null,
#[Map(if: new IsNotNull())]
public ?string $email = null,
) {
}
}
Mapping new UserPatch('Alice') onto an existing user updates only the name
property. The email property is left untouched because its value is null.
Map Collections to a Specific Class
The MapCollection transform maps an array of source objects into target
objects. Until now, each item class required its own #[Map] attribute.
Symfony 8.1 adds a targetClass option that lets you declare the destination
class directly on the collection:
1 2 3 4 5 6 7 8 9
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Transform\MapCollection;
#[Map(target: Order::class)]
class OrderInput
{
#[Map(transform: new MapCollection(targetClass: LineItem::class))]
public array $items = [];
}
Each element of $items is mapped to a LineItem instance. This is useful
when the same source objects are reused in different contexts and you don't want
to add mapping metadata to shared DTOs.
Match Multiple Source and Target Classes
The TargetClass condition restricts a mapping to specific destination
classes. Previously, mapping a property to several target classes required one
#[Map] attribute per class. Symfony 8.1 lets you pass an array of class
names so a single condition can match any of them:
1 2 3 4 5 6 7 8 9 10 11 12
use Symfony\Component\ObjectMapper\Attribute\Map;
use Symfony\Component\ObjectMapper\Condition\TargetClass;
#[Map(target: PublicProfile::class)]
#[Map(target: AdminProfile::class)]
#[Map(target: AuditProfile::class)]
class User
{
// map 'lastLoginIp' only when targeting an admin or audit profile
#[Map(target: 'ip', if: new TargetClass([AdminProfile::class, AuditProfile::class]))]
public ?string $lastLoginIp = null;
}
The same array syntax is supported by the new SourceClass condition, which
matches when the source object is an instance of any of the specified classes.
Clearer Errors for Invalid Transform Callables
Previously, if a transform or if callable was misconfigured (for example,
a method that didn't exist or a service that didn't implement the expected interface),
the value was silently left unmapped, making the issue difficult to diagnose.
ObjectMapper now throws a NoSuchCallableException instead:
1 2 3 4 5 6 7 8 9
use Symfony\Component\ObjectMapper\Attribute\Map;
class ProductInput
{
// 'wrongMethod' is not callable: Symfony 8.1 throws an exception
// instead of silently skipping the transformation
#[Map(transform: 'wrongMethod')]
public string $name = '';
}
The exception message explains the problem and reminds you that callable classes
must implement TransformCallableInterface (or ConditionCallableInterface
for the if option).
Merge Nested Objects Into the Same Target
Source DTOs are often nested while the target object is flat. When a nested source object maps to the same target class as its parent, ObjectMapper now merges its properties directly into the same target instance, flattening the structure for you:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
use Symfony\Component\ObjectMapper\Attribute\Map;
#[Map(target: User::class)]
class UserInput
{
public string $name;
// 'address' also maps to User, so its properties are
// merged into the same User instance
public AddressInput $address;
}
#[Map(target: User::class)]
class AddressInput
{
#[Map(target: 'streetName')]
public string $street;
public string $city;
}
Mapping a UserInput writes street and city from the nested
address object into the same User instance alongside name, with no
manual flattening required.