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

Florent Blaison
Contributed by Florent Blaison in #62522

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

Julien Robic
Contributed by Julien Robic in #63595

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

Antoine Bluchet
Contributed by Antoine Bluchet in #63024

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

Ryan RAJKOMAR
Contributed by Ryan RAJKOMAR in #63383

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

calm329 Michaël Marinetti
Contributed by calm329 and Michaël Marinetti in #62957

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

Antoine Bluchet
Contributed by Antoine Bluchet in #62511

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.

Published in #Living on the edge