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.
Install the package via Composer:
composer require symfony/object-mapper
Register the service in config/app.php or a service provider:
$app->singleton(Symfony\Component\PropertyAccess\PropertyAccess::class);
$app->singleton(Symfony\Component\ObjectMapper\ObjectMapperInterface::class, function ($app) {
return new Symfony\Component\ObjectMapper\ObjectMapper();
});
First use case: Basic mapping Define source and target classes with attributes:
use Symfony\Component\ObjectMapper\Annotation\MapEntity;
#[MapEntity]
class UserEntity {
public function __construct(
public string $name,
public string $email
) {}
}
#[MapEntity]
class UserDto {
public function __construct(
public string $fullName,
public string $contactEmail
) {}
}
Map in a service/controller:
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
class UserService {
public function __construct(private ObjectMapperInterface $mapper) {}
public function mapEntityToDto(UserEntity $entity): UserDto {
return $this->mapper->map($entity, UserDto::class);
}
}
Key first steps:
#[MapEntity] on classes to enable mapping.ObjectMapperInterface into services.$mapper->map($source, $targetClass) for basic transformations.#[MapEntity]
class PostEntity {
public function __construct(
public int $id,
public string $title,
public string $content,
public ?UserEntity $author = null
) {}
}
#[MapEntity]
class PostDto {
public function __construct(
public int $id,
public string $title,
public string $summary,
public ?UserDto $author = null
) {}
}
// In controller:
$dto = $mapper->map($postEntity, PostDto::class);
$dto->summary = Str::limit($postEntity->content, 100);
Tip: Use #[Ignore] on properties to exclude them from mapping.
#[MapEntity]
class Order {
public function __construct(
public int $id,
public array $items,
public ?Address $shippingAddress = null
) {}
}
#[MapEntity]
class OrderDto {
public function __construct(
public int $id,
public array $items,
public ?AddressDto $shippingAddress = null
) {}
}
// Automatically maps nested `Address` → `AddressDto`.
#[When]use Symfony\Component\ObjectMapper\Annotation\When;
#[MapEntity]
class UserDto {
public function __construct(
#[When('isAdmin', condition: 'source->isAdmin()')]
public ?string $adminRole = null
) {}
}
#[MapEntity]
class UserCollectionDto {
public function __construct(
public array $users
) {}
}
$collectionDto = $mapper->map($userEntities, UserCollectionDto::class);
use Symfony\Component\ObjectMapper\Transformer\TransformerInterface;
class DateTransformer implements TransformerInterface {
public function transform($value, string $targetType, array $context = []): ?string {
return $value?->format('Y-m-d');
}
}
// Register in ObjectMapper:
$mapper->addTransformer(DateTransformer::class);
Use case: Transform DateTime objects to strings in DTOs.
Use #[Attribute] for PHP 8+ compatibility:
use Symfony\Component\ObjectMapper\Annotation\MapEntity as MapEntityAttribute;
#[MapEntityAttribute]
class UserDto { ... }
Leverage Laravel’s make: command for DTOs:
Create a custom command to scaffold DTOs with #[MapEntity] attributes.
Combine with API Resources:
use Illuminate\Http\Resources\Json\JsonResource;
class UserResource extends JsonResource {
public function toArray($request) {
return $this->mapper->map($this->resource, UserDto::class);
}
}
Cache Mappings for Performance:
$mapper = new ObjectMapper();
$mapper->setCache(new Symfony\Component\Cache\Adapter\FilesystemAdapter());
If using Symfony’s Messenger, map messages to commands:
$command = $mapper->map($message, CreateUserCommand::class);
$bus->dispatch($command);
Use with API Platform for automatic DTO generation:
# config/packages/api_platform.yaml
api_platform:
formats:
jsonld:
mime_types: ['application/ld+json']
object_mapper: true
Attribute Reflection Caching:
php artisan optimize may break attribute reflection if not configured properly.opcache.enable=1 in php.ini or use #[Attribute] instead of #[MapEntity] for PHP 8+.Circular References:
User ↔ Post) causes infinite loops.#[Ignore] or implement a custom transformer with cycle detection:
$mapper->addTransformer(new CircularReferenceTransformer());
Constructor Arguments:
#[MapEntity] on the class and properties:
#[MapEntity]
class UserDto {
#[MapEntity]
public string $name; // Explicitly mapped
}
Lazy-Loaded Properties:
User#posts) fail during mapping.$user->initializeLazyCollections();
$dto = $mapper->map($user, UserDto::class);
Type Mismatches:
string → int).#[MapEntity(type: 'int')].Enable Debug Mode:
$mapper = new ObjectMapper();
$mapper->setDebug(true); // Logs mapping attempts
Inspect Mapping Metadata:
$metadata = $mapper->getMetadataFactory()->getMetadataFor(UserDto::class);
dump($metadata->getProperties());
Handle Missing Properties:
#[MapEntity(default: 'null')] or implement a fallback transformer:
$mapper->addTransformer(new NullFallbackTransformer());
Custom Metadata Factory:
Override MetadataFactoryInterface to add custom rules:
$factory = new CustomMetadataFactory();
$mapper->setMetadataFactory($factory);
Event Listeners: Attach listeners for pre/post-mapping logic:
$mapper->addListener(new LoggingListener());
Dynamic Mapping:
Use #[When] conditions for runtime decisions:
#[When('isActive', condition: 'source->isActive()')]
public ?string $status = null;
Laravel Service Provider:
Extend the mapper in AppServiceProvider:
public function register() {
$this->app->extend(ObjectMapperInterface::class, function ($mapper) {
$mapper->addTransformer(new LaravelSpecificTransformer());
return $mapper;
});
}
Attribute Caching:
$mapper->setCache(new Symfony\Component\Cache\Adapter\ArrayAdapter());
Avoid Reflection Overhead:
Batch Mapping:
ArrayTransformer for collections:
$mapper->map($entities, UserDto::class, [], [ArrayTransformer::class]);
belongsTo/hasMany relationships may not map automatically.#[MapEntity] on related models or custom transformHow can I help you explore Laravel packages today?