Symfony provides a mapper to transform a given object to another one. This compoent is experimental.
Run this command to install the object-mapper
before using it:
$ composer require symfony/object-mapper
Once installed, the object mapper service can be injected in any service where you need it or it can be used in a controller:
// src/Controller/DefaultController.php namespace App\Controller; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\ObjectMapper\ObjectMapperInterface; class DefaultController extends AbstractController { public function __invoke(ObjectMapperInterface $objectMapper): Response { // keep reading for usage examples } }
To map an object to another one use map
:
use App\Entity\Book; use App\ValueObject\Book as BookDto; $book = $bookRepository->find(1); $mapper = new ObjectMapper(); $mapper->map($book, BookDto::class);
If you already have a target object, you can use its instance directly:
use App\Entity\Book; use App\ValueObject\Book as BookDto; $bookDto = new BookDto(title: 'An updated title'); $book = $bookRepository->find(1); $mapper = new ObjectMapper(); $mapper->map($bookDto, $book);
The Object Mapper source can also be a stdClass:
use App\Entity\Book; $bookDto = new \stdClass(); $bookDto->title = 'An updated title'; $mapper = new ObjectMapper(); $mapper->map($bookDto, Book::class);
The Object Mapper component includes a :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute to configure mapping behavior between objects. Use this attribute on a class to specify the target class:
// src/Dto/Source.php namespace App\Dto; use Symfony\Component\ObjectMapper\Attributes\Map; #[Map(target: Target::class)] class Source {}
Use the :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute on properties to configure property mapping between
objects. target
changes the target property, if
allows to
conditionally map properties:
// src/Dto/Source.php namespace App\Dto; use Symfony\Component\ObjectMapper\Attributes\Map; class Source { #[Map(target: 'fullName')] public string $firstName; // when we do not want to map the lastName we can use `false` #[Map(if: false)] public string $lastName; }
When a property is not present in the target class it will be ignored.
The condition mapping can also be configured as a service to do so implement a :class:`Symfony\\Component\\ObjectMapper\\ConditionCallableInterface`:
// src/ObjectMapper/ConditionNameCallable.php namespace App\ObjectMapper; use App\Dto\Source; use Symfony\Component\ObjectMapper\ConditionCallableInterface; /** * @implements ConditionCallableInterface<Source> */ final class ConditionNameCallable implements ConditionCallableInterface { public function __invoke(mixed $value, object $source): bool { return is_string($value); } } // src/Dto/Source.php namespace App\Dto; use App\ObjectMapper\ConditionNameCallable; use Symfony\Component\ObjectMapper\Attributes\Map; class Source { #[Map(if: ConditionCallableInterface::class)] public mixed $status; }
Whe you have multiple mapping targets, you can also use the target class name as a condition for property mapping:
#[Map(target: B::class)] #[Map(target: C::class)] class A { // This will map to `foo` only when the target is of type B::class #[Map(target: 'somethingOnlyInB', transform: 'strtoupper', if: B::class)] public string $something = 'test'; }
Use transform
to call a static function or a
:class:`Symfony\\Component\\ObjectMapper\\TransformCallableInterface`:
// src/ObjectMapper/TransformNameCallable.php namespace App\ObjectMapper; use App\Dto\Source; use Symfony\Component\ObjectMapper\TransformCallableInterface; /** * @implements TransformCallableInterface<Source> */ final class TransformNameCallable implements TransformCallableInterface { public function __invoke(mixed $value, object $source): mixed { return sprintf('%s %s', $source->firstName, $source->lastName); } } // src/Dto/Source.php namespace App\Dto; use App\ObjectMapper\TransformNameCallable; use Symfony\Component\ObjectMapper\Attributes\Map; class Source { #[Map(target: 'fullName', transform: TransformNameCallable::class)] public string $firstName; }
We can also use a transformation mapping on a class, it should return the type of your mapping target:
// src/Dto/Source.php namespace App\Dto; #[Map(transform: [Target::class, 'newInstance'])] class Source { public string $name = 'test'; } // src/Dto/Target.php class Target { public ?string $name = null; public function __construct(private readonly int $id) { } public function getId(): int { return $this->id; } public static function newInstance(): self { return new self(1); } }
The if
and transform
parameters also accept static callbacks:
// src/Dto/Source.php namespace App\Dto; use Symfony\Component\ObjectMapper\Attributes\Map; class Source { #[Map(if: 'boolval', transform: 'ucfirst')] public ?string $lastName = null; }
The :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute works on classes and it can be repeated:
// src/Dto/Source.php namespace App\Dto; use App\Dto\B; use App\Dto\C; use App\ObjectMapper\TransformNameCallable; use Symfony\Component\ObjectMapper\Attributes\Map; #[Map(target: B::class, if: [Source::class, 'shouldMapToB'])] #[Map(target: C::class, if: [Source::class, 'shouldMapToC'])] class Source { /** * In case of a condition on a class, $value will be null */ public static function shouldMapToB(mixed $value, object $source): bool { return false; } public static function shouldMapToC(mixed $value, object $source): bool { return true; } }
The :class:`Symfony\\Component\\ObjectMapper\\ObjectMapperMetadataFactoryInterface` allows to change how mapping metadata is computed. With this interface we can create a MapStruct version of the Object Mapper:
// src/ObjectMapper/Metadata/MapStructMapperMetadataFactory.php namespace App\Metadata\ObjectMapper; use Symfony\Component\ObjectMapper\Attribute\Map; use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface; use Symfony\Component\ObjectMapper\Metadata\Mapping; use Symfony\Component\ObjectMapper\ObjectMapperInterface; /** * A Metadata factory that implements the basics behind https://mapstruct.org/. */ final class MapStructMapperMetadataFactory implements ObjectMapperMetadataFactoryInterface { public function __construct(private readonly string $mapper) { if (!is_a($mapper, ObjectMapperInterface::class, true)) { throw new \RuntimeException(sprintf('Mapper should implement "%s".', ObjectMapperInterface::class)); } } public function create(object $object, ?string $property = null, array $context = []): array { $refl = new \ReflectionClass($this->mapper); $mapTo = []; $source = $property ?? $object::class; foreach (($property ? $refl->getMethod('map') : $refl)->getAttributes(Map::class) as $mappingAttribute) { $map = $mappingAttribute->newInstance(); if ($map->source === $source) { $mapTo[] = new Mapping($map->source, $map->target, $map->if, $map->transform); continue; } } // Default is to map properties to a property of the same name if (!$mapTo && $property) { $mapTo[] = new Mapping($property, $property); } return $mapTo; } }
With this metadata usage, the mapping definition can be written as a service:
// src/ObjectMapper/AToBMapper namespace App\Metadata\ObjectMapper; use App\Dto\Source; use App\Dto\Target; use Symfony\Component\ObjectMapper\Attributes\Map; use Symfony\Component\ObjectMapper\ObjectMapper; use Symfony\Component\ObjectMapper\ObjectMapperInterface; #[Map(source: Source::class, target: Target::class)] class AToBMapper implements ObjectMapperInterface { public function __construct(private readonly ObjectMapper $objectMapper) { } #[Map(source: 'propertyA', target: 'propertyD')] #[Map(source: 'propertyB', if: false)] public function map(object $source, object|string|null $target = null): object { return $this->objectMapper->map($source, $target); } }
The custom metadata is injected into our :class:`Symfony\\Component\\ObjectMapper\\ObjectMapperInterface`:
$a = new Source('a', 'b', 'c'); $metadata = new MapStructMapperMetadataFactory(AToBMapper::class); $mapper = new ObjectMapper($metadata); $aToBMapper = new AToBMapper($mapper); $b = $aToBMapper->map($a);