symfony/object-mapper
Symfony Object Mapper component maps data from one object to another using PHP attributes. Simplifies DTO/entity transformations, supports configurable mapping rules, and integrates with the Symfony ecosystem. Documentation and contributions are handled in the main Symfony repository.
Installation:
composer require symfony/object-mapper
For Laravel, register the service in config/services.php:
'object_mapper' => Symfony\Component\ObjectMapper\ObjectMapper::class,
Bind it in a service provider:
$this->app->singleton(Symfony\Component\ObjectMapper\ObjectMapper::class, function ($app) {
return new Symfony\Component\ObjectMapper\ObjectMapper();
});
First Use Case: Map a source object to a target DTO using attributes:
use Symfony\Component\ObjectMapper\Annotations as ORM;
#[ORM\MapEntity]
class UserResponseDTO {
#[ORM\MapProperty("name")]
public string $fullName;
#[ORM\MapProperty("email")]
public string $emailAddress;
}
// In a Laravel controller/service:
$mapper = app(Symfony\Component\ObjectMapper\ObjectMapper::class);
$dto = $mapper->map(new User(), UserResponseDTO::class);
Quick Start Guide:
@MapEntity on the target class.@MapProperty to specify source fields.@MapCollection or @MapEmbedded.// Source entity
class User {
public string $name;
public string $email;
}
// Target DTO
#[MapEntity]
class UserDTO {
#[MapProperty("name")]
public string $fullName;
#[MapProperty("email")]
public string $email;
}
// Usage
$mapper->map($user, UserDTO::class);
#[MapEntity]
class ProfileDTO {
#[MapEmbedded]
public AddressDTO $address;
}
#[MapEntity]
class AddressDTO {
#[MapProperty("street")]
public string $streetName;
}
// Maps `User` → `ProfileDTO` with nested `AddressDTO`.
#[MapEntity]
class UserCollectionDTO {
#[MapCollection]
public array $users;
}
// Maps `Collection<User>` → `array<UserDTO>`.
#[MapEntity]
class AdminUserDTO {
#[MapProperty("name")]
public string $adminName;
#[MapProperty("email")]
public string $email;
}
// Only map to `AdminUserDTO` if `$user->isAdmin()`.
$mapper->map($user, AdminUserDTO::class, [
'condition' => fn ($source) => $source->isAdmin(),
]);
#[MapEntity]
class UserWithAgeDTO {
#[MapProperty("name")]
public string $name;
#[MapProperty("birthDate")]
#[MapTransform(fn ($date) => Carbon::parse($date)->age)]
public int $age;
}
public function register(): void {
$this->app->singleton(Symfony\Component\ObjectMapper\ObjectMapper::class, function ($app) {
$mapper = new Symfony\Component\ObjectMapper\ObjectMapper();
// Optional: Register custom transforms or conditions.
return $mapper;
});
}
public function show(User $user) {
$dto = app(Symfony\Component\ObjectMapper\ObjectMapper::class)
->map($user, UserResponseDTO::class);
return response()->json($dto);
}
public function update(StoreUserRequest $request) {
$data = $request->validated();
$user = $mapper->map($data, User::class);
$user->save();
}
public function handle(OrderCreatedEvent $event) {
$command = $mapper->map($event->order, OrderCommand::class);
// Dispatch $command...
}
public function testMapping() {
$user = new User(['name' => 'John', 'email' => 'john@example.com']);
$dto = $mapper->map($user, UserDTO::class);
$this->assertEquals('John', $dto->fullName);
}
Attribute Reflection Overhead:
spatie/laravel-data.$mapper = new ObjectMapper([
'cache_attributes' => true,
]);
Circular References:
User ↔ Profile) may cause infinite loops.@MapEmbedded with lazy: true or implement a custom condition to break cycles.Missing Properties:
@MapProperty(ignore_missing: true) or handle it in a custom transform.Constructor Arguments:
@MapConstructor or ensure properties are public/accessible.Nested Collections:
User → Profile → Address[]) can be slow.@MapCollection with index_by for performance:
#[MapCollection(index_by: 'id')]
public array $addresses;
Type Mismatches:
string → int).@MapTransform or a custom TypeConverterInterface.Enable Debug Mode:
$mapper = new ObjectMapper([
'debug' => true,
]);
stderr.Inspect Mapping Rules:
$metadata = $mapper->getMetadata(UserDTO::class);
dump($metadata->getProperties());
Handle Exceptions:
MappingException for invalid mappings:
try {
$mapper->map($source, Target::class);
} catch (MappingException $e) {
Log::error('Mapping failed: ' . $e->getMessage());
}
Custom Conditions:
#[MapEntity]
class PremiumUserDTO {
#[MapProperty("name")]
public string $name;
#[MapProperty("subscription")]
public SubscriptionDTO $subscription;
#[MapCondition(fn ($source) => $source->isPremium())]
public function isPremium(): bool {}
}
Custom Transformers:
$mapper->addTransformer(new class implements TypeConverterInterface {
public function convert($value, string $targetType, array $context = []): mixed {
return Carbon::parse($value);
}
});
Class-Level Conditions:
#[MapEntity]
#[MapCondition(fn ($source) => $source->isActive())]
class ActiveUserDTO {}
Lazy Loading:
#[MapEmbedded(lazy: true)]
public ProfileDTO $profile;
Custom Metadata:
$mapper->setMetadata(UserDTO::class, new Metadata(
new PropertyMetadata('name', 'fullName'),
// ...
));
Integration with Laravel:
ObjectMapperAwareInterface for DI:
class MyService implements ObjectMapperAwareInterface {
public function setObjectMapper(ObjectMapper $mapper): void {
$this->mapper = $mapper;
}
}
How can I help you explore Laravel packages today?