symfony/object-mapper
Symfony Object Mapper maps one object to another using PHP attributes, giving you a clean, declarative way to handle DTOs and transformations without boilerplate. Part of the Symfony ecosystem.
symfony/object-mapper is a small, framework-agnostic, attribute-driven object-to-object mapper. In a Laravel app it is a library, not a Laravel package: there is no service provider, facade, config file, validation layer, or auto-discovery. Treat it as a lower-level tool you wire yourself.
Install a version compatible with your PHP runtime. The 8.x line requires PHP 8.4; 7.4 is the safer choice for typical Laravel apps on PHP 8.2/8.3. Pin a recent patch — bugfixes land frequently and the early patches of each minor were rough:
composer require symfony/object-mapper:^7.4
# or, on PHP 8.4:
composer require symfony/object-mapper:^8.1.8
Then bind the mapper as a singleton. The constructor is small but has changed across versions, so verify vendor/symfony/object-mapper/ObjectMapper.php after install:
// app/Providers/AppServiceProvider.php
use Symfony\Component\ObjectMapper\ObjectMapper;
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
use Symfony\Component\ObjectMapper\Metadata\ClassMetadataFactory;
public function register(): void
{
$this->app->singleton(ObjectMapperInterface::class, function () {
// Check the installed constructor; this is the common shape.
return new ObjectMapper(new ClassMetadataFactory());
});
}
First use case: replace hand-written API DTO construction in a controller.
use Symfony\Component\ObjectMapper\Attribute\Map;
final class UserData
{
public int $id;
#[Map(source: 'firstName')]
public string $givenName;
#[Map(source: 'lastName')]
public string $familyName;
}
// Controller
public function show(User $user, ObjectMapperInterface $mapper): UserData
{
return $mapper->map($user, UserData::class);
}
Where to look first:
https://symfony.com/doc/current/object_mapper.htmlMap attribute class: vendor/symfony/object-mapper/Attribute/Map.phpvendor/symfony/object-mapper/Tests if shipped in your installUse it at the boundary, not in the domain. The best day-to-day pattern is a thin adapter service per aggregate or use case. Controllers and jobs depend on your adapter, not directly on ObjectMapperInterface:
final class UserDataMapper
{
public function __construct(private ObjectMapperInterface $mapper) {}
public function fromModel(User $user): UserData
{
return $this->mapper->map($user, UserData::class);
}
}
This gives you one place to test mapping rules and one place to swap the implementation later. It is also the natural place to wrap the mapper yourself (logging, timing, null handling) instead of leaning on Symfony’s DI decoration — see the lazy-ghost note in Gotchas.
Response DTOs are the highest-value use case. Map Eloquent models to API resource DTOs instead of returning models directly or manually constructing arrays. Keep DTOs explicit; do not expose every model column by convention just because the names match.
Use transformers for value objects and type changes. The mapper does not magically cast strings to DateTimeImmutable, enums, or money objects. Add a transformer:
final class DateTransformer
{
public static function toImmutable(?string $value): ?\DateTimeImmutable
{
return $value ? new \DateTimeImmutable($value) : null;
}
}
final class OrderData
{
#[Map(source: 'created_at', transform: [DateTransformer::class, 'toImmutable'])]
public \DateTimeImmutable $createdAt;
}
Use constructor promotion for immutable DTOs. Readonly/promoted properties work best when the target is built through its constructor. Put #[Map(source: '...')] on the promoted property/parameter and keep constructor parameter names aligned with source properties where possible.
Handle polymorphism with if: conditions, not controller branches. Multiple target classes are supported via class-level #[Map(target: ..., if: ...)]. Keep those conditions small and covered by tests. If you find yourself writing complex expression strings, move the decision into a dedicated mapper service instead.
Nested objects and collections. Nested DTOs can be mapped when types line up. For collections, prefer an explicit transformer until you have verified the collection behavior for your version; the 7.4 line added collection transformer support, but collections are where version-specific bugs tend to appear. Transformers that need to map nested objects can implement ObjectMapperAwareInterface to receive the owning mapper.
Mapping onto an existing object is overwrite-oriented. $mapper->map($input, $user) mutates the target. It is not a PATCH merge. For partial updates, map to a DTO with nullable fields and apply only non-null values explicitly, or use conditions to skip properties.
Use it in tests and factories. Mapping fixtures to domain objects is a clean way to reduce test setup boilerplate, especially for command/query objects and event payloads.
Octane and long-running workers. Bind the mapper as a singleton. Attribute metadata is cached in memory in recent versions, which is good for Octane, but deploy-time code reloads still matter. Do not store request state in the mapper or in custom transformers.
config/object_mapper.php, no Artisan command, and no validation. Pair it with FormRequests for input validation.$request->validated() cannot be passed directly. Build a source DTO/command explicitly, or use a Laravel-native data package for request hydration.toArray(). Be explicit with #[Map(source: '...')], eager-load relationships to avoid N+1 queries, and test accessors/casts carefully.$fillable, mutators, events, and validation. Never map untrusted request data directly onto models. Map to a DTO, validate it, then persist explicitly.ObjectMapperInterface yourself — which this assessment recommends — be aware that nested-object construction paths were affected on earlier 8.1.x patches, so verify nested mappings through your wrapper with a test rather than assuming the inner mapper is reached transparently.source: or target: will not be caught by PHPStan or your IDE. Write one mapping test per DTO; use class constants for class names where possible.ObjectMapperAwareInterface when a transformer needs to recursively map.bug #… fixes to nested/constructor/decorator edge cases. Before bumping, skim the changelog between your pin and the target version, and keep a small suite of mapping tests per DTO so regressions surface immediately.symfony/symfony monorepo. Check monorepo PRs and changelogs, and treat the component as young but actively maintained.spatie/laravel-data or hand-written mapping should be a small change, not a rewrite.How can I help you explore Laravel packages today?