symfony/object-mapper
Symfony Object Mapper maps data between objects (e.g., DTOs to entities) using PHP attributes to define field mappings. It reduces boilerplate transformation code, supports configurable mapping logic, and integrates cleanly with Symfony applications.
Since Laravel doesn’t natively support Symfony’s ObjectMapper, you’ll need to:
composer require symfony/object-mapper
config/app.php (if using Symfony components):
'providers' => [
// ...
Symfony\Component\ObjectMapper\ObjectMapper::class,
],
use Symfony\Component\ObjectMapper\ObjectMapper;
use Symfony\Component\ObjectMapper\Attribute\Map;
// Define source and target classes with [Map] attributes
#[Map]
class UserDto {
public function __construct(
#[Map(source: 'name')]
public string $fullName,
#[Map(source: 'email')]
public string $emailAddress,
) {}
}
// Map from an array or another object
$mapper = new ObjectMapper();
$dto = $mapper->map([
'name' => 'John Doe',
'email' => 'john@example.com',
], UserDto::class);
use Symfony\Component\ObjectMapper\Attribute\Map;
#[Map]
class CreateUserRequest {
public function __construct(
#[Map(source: 'first_name')]
public string $name,
#[Map(source: 'email_address')]
public string $email,
) {}
}
// In a Laravel controller:
$mapper = app(ObjectMapper::class);
$requestData = $this->request->all();
$userRequest = $mapper->map($requestData, CreateUserRequest::class);
Use Symfony’s [Map] attributes to define transformations declaratively:
#[Map]
class OrderDto {
public function __construct(
#[Map(source: 'order_id')]
public int $id,
#[Map(source: 'customer.name')] // Nested property
public string $customerName,
#[Map(source: 'items', type: OrderItemDto::class)] // Collection mapping
public array $items,
) {}
}
Skip fields based on runtime conditions:
#[Map(condition: fn ($source) => $source['is_active'] ?? false)]
class UserDto {
#[Map(source: 'role', condition: fn ($source) => $source['is_admin'] ?? false)]
public ?string $role;
}
Map complex hierarchies (e.g., User → UserDto with embedded AddressDto):
#[Map]
class UserDto {
public function __construct(
#[Map(source: 'address', type: AddressDto::class)]
public AddressDto $address,
) {}
}
#[Map]
class AddressDto {
public function __construct(
#[Map(source: 'street')]
public string $street,
) {}
}
Map arrays to collections of objects:
#[Map]
class OrderDto {
#[Map(source: 'items', type: OrderItemDto::class)]
public array $items;
}
Extend functionality via TransformerInterface:
use Symfony\Component\ObjectMapper\Transformer\TransformerInterface;
class CustomDateTransformer implements TransformerInterface {
public function transform($value, string $targetType, ?string $targetProperty): mixed {
return \DateTime::createFromFormat('Y-m-d', $value);
}
}
// Register in ObjectMapper:
$mapper->addTransformer(new CustomDateTransformer());
AppServiceProvider:
public function register(): void {
$this->app->singleton(ObjectMapper::class, fn () => new ObjectMapper());
}
use Symfony\Component\ObjectMapper\Attribute\Map;
class StoreUserRequest extends FormRequest {
#[Map]
public function rules(): array {
return [
'name' => 'required|string',
'email' => 'required|email',
];
}
public function map(): UserDto {
return $this->objectMapper->map($this->validated(), UserDto::class);
}
}
Symfony Dependency Lock-In:
Attribute Overhead:
Lazy Loading Issues:
#[Map(lazy: false)] or manually initialize properties.Constructor Parameter Conflicts:
#[Map(ignore_missing: true)] or adjust the target class.Circular References:
User ↔ Order) can cause infinite loops.#[Map(circular: true)] or break cycles manually.Performance with Large Collections:
#[Map(batch: 100)] or process in chunks.$mapper = new ObjectMapper();
$mapper->setDebug(true); // Logs mapping steps
[Map] attributes.#[MapMetadata]:
#[MapMetadata('user', UserDto::class)]
class UserMapper {
public function map($source): UserDto {
return new UserDto($source['name'], $source['email']);
}
}
$mapper->addListener(new class implements MapperListenerInterface {
public function onMap(object $source, string $targetClass, array $context): void {
// Pre-mapping logic
}
});
$mapper->setCache(new \Symfony\Component\Cache\Adapter\FilesystemAdapter());
ObjectMapper vs. Illuminate\Database\Eloquent\Model).
Fix: Use a custom alias in config/app.php:
'aliases' => [
'symfony.object_mapper' => Symfony\Component\ObjectMapper\ObjectMapper::class,
],
ObjectMapper in views (tight coupling). Instead, map data in controllers and pass DTOs to views.#[Map(ignore_missing: true)]:
Skip unmapped properties instead of failing.$mapper->map($chunk, UserDto::class, ['batch' => 50]);
How can I help you explore Laravel packages today?