Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 02d7362

Browse filesBrowse files
committed
[Object Mapper] component introduction
1 parent c9dc94b commit 02d7362
Copy full SHA for 02d7362

File tree

1 file changed

+346
-0
lines changed
Filter options

1 file changed

+346
-0
lines changed

‎object-mapper.rst

Copy file name to clipboard
+346Lines changed: 346 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,346 @@
1+
Object Mapper
2+
==============
3+
4+
Symfony provides a mapper to transform a given object to another one.
5+
This compoent is experimental.
6+
7+
Installation
8+
------------
9+
10+
Run this command to install the ``object-mapper`` before using it:
11+
12+
.. code-block:: terminal
13+
14+
$ composer require symfony/object-mapper
15+
16+
Using the ObjectMapper Service
17+
------------------------------
18+
19+
Once installed, the object mapper service can be injected in any service where
20+
you need it or it can be used in a controller::
21+
22+
// src/Controller/DefaultController.php
23+
namespace App\Controller;
24+
25+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
26+
use Symfony\Component\HttpFoundation\Response;
27+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
28+
29+
class DefaultController extends AbstractController
30+
{
31+
public function __invoke(ObjectMapperInterface $objectMapper): Response
32+
{
33+
// keep reading for usage examples
34+
}
35+
}
36+
37+
38+
Map an object to another one
39+
----------------------------
40+
41+
To map an object to another one use ``map``::
42+
43+
use App\Entity\Book;
44+
use App\ValueObject\Book as BookDto;
45+
46+
$book = $bookRepository->find(1);
47+
$mapper = new ObjectMapper();
48+
$mapper->map($book, BookDto::class);
49+
50+
51+
If you already have a target object, you can use its instance directly::
52+
53+
use App\Entity\Book;
54+
use App\ValueObject\Book as BookDto;
55+
56+
$bookDto = new BookDto(title: 'An updated title');
57+
$book = $bookRepository->find(1);
58+
$mapper = new ObjectMapper();
59+
$mapper->map($bookDto, $book);
60+
61+
The Object Mapper source can also be a `stdClass`::
62+
63+
use App\Entity\Book;
64+
65+
$bookDto = new \stdClass();
66+
$bookDto->title = 'An updated title';
67+
$mapper = new ObjectMapper();
68+
$mapper->map($bookDto, Book::class);
69+
70+
Configure the mapping target using attributes
71+
---------------------------------------------
72+
73+
The Object Mapper component includes a :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute to configure mapping
74+
behavior between objects. Use this attribute on a class to specify the
75+
target class::
76+
77+
// src/Dto/Source.php
78+
namespace App\Dto;
79+
80+
use Symfony\Component\ObjectMapper\Attributes\Map;
81+
82+
#[Map(target: Target::class)]
83+
class Source {}
84+
85+
Configure property mapping
86+
--------------------------
87+
88+
Use the :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute on properties to configure property mapping between
89+
objects. ``target`` changes the target property, ``if`` allows to
90+
conditionally map properties::
91+
92+
// src/Dto/Source.php
93+
namespace App\Dto;
94+
95+
use Symfony\Component\ObjectMapper\Attributes\Map;
96+
97+
class Source {
98+
#[Map(target: 'fullName')]
99+
public string $firstName;
100+
101+
// when we do not want to map the lastName we can use `false`
102+
#[Map(if: false)]
103+
public string $lastName;
104+
}
105+
106+
When a property is not present in the target class it will be ignored.
107+
108+
The condition mapping can also be configured as a service
109+
to do so implement a :class:`Symfony\\Component\\ObjectMapper\\ConditionCallableInterface`::
110+
111+
// src/ObjectMapper/ConditionNameCallable.php
112+
namespace App\ObjectMapper;
113+
114+
use App\Dto\Source;
115+
use Symfony\Component\ObjectMapper\ConditionCallableInterface;
116+
117+
/**
118+
* @implements ConditionCallableInterface<Source>
119+
*/
120+
final class ConditionNameCallable implements ConditionCallableInterface
121+
{
122+
public function __invoke(mixed $value, object $source): bool
123+
{
124+
return is_string($value);
125+
}
126+
}
127+
128+
// src/Dto/Source.php
129+
namespace App\Dto;
130+
131+
use App\ObjectMapper\ConditionNameCallable;
132+
use Symfony\Component\ObjectMapper\Attributes\Map;
133+
134+
class Source {
135+
#[Map(if: ConditionCallableInterface::class)]
136+
public mixed $status;
137+
}
138+
139+
Whe you have multiple mapping targets, you can also use the target class name as a condition for property mapping::
140+
141+
#[Map(target: B::class)]
142+
#[Map(target: C::class)]
143+
class A
144+
{
145+
// This will map to `foo` only when the target is of type B::class
146+
#[Map(target: 'somethingOnlyInB', transform: 'strtoupper', if: B::class)]
147+
public string $something = 'test';
148+
}
149+
150+
151+
Transform mapped values
152+
-----------------------
153+
154+
Use ``transform`` to call a static function or a
155+
:class:`Symfony\\Component\\ObjectMapper\\TransformCallableInterface`::
156+
157+
// src/ObjectMapper/TransformNameCallable.php
158+
namespace App\ObjectMapper;
159+
160+
use App\Dto\Source;
161+
use Symfony\Component\ObjectMapper\TransformCallableInterface;
162+
163+
/**
164+
* @implements TransformCallableInterface<Source>
165+
*/
166+
final class TransformNameCallable implements TransformCallableInterface
167+
{
168+
public function __invoke(mixed $value, object $source): mixed
169+
{
170+
return sprintf('%s %s', $source->firstName, $source->lastName);
171+
}
172+
}
173+
174+
// src/Dto/Source.php
175+
namespace App\Dto;
176+
177+
use App\ObjectMapper\TransformNameCallable;
178+
use Symfony\Component\ObjectMapper\Attributes\Map;
179+
180+
class Source {
181+
#[Map(target: 'fullName', transform: TransformNameCallable::class)]
182+
public string $firstName;
183+
}
184+
185+
We can also use a transformation mapping on a class, it should return the type of your mapping target::
186+
187+
// src/Dto/Source.php
188+
namespace App\Dto;
189+
190+
#[Map(transform: [Target::class, 'newInstance'])]
191+
class Source
192+
{
193+
public string $name = 'test';
194+
}
195+
196+
197+
// src/Dto/Target.php
198+
class Target
199+
{
200+
public ?string $name = null;
201+
202+
public function __construct(private readonly int $id)
203+
{
204+
}
205+
206+
public function getId(): int
207+
{
208+
return $this->id;
209+
}
210+
211+
public static function newInstance(): self
212+
{
213+
return new self(1);
214+
}
215+
}
216+
217+
218+
The ``if`` and ``transform`` parameters also accept static callbacks::
219+
220+
// src/Dto/Source.php
221+
namespace App\Dto;
222+
223+
use Symfony\Component\ObjectMapper\Attributes\Map;
224+
225+
class Source {
226+
#[Map(if: 'boolval', transform: 'ucfirst')]
227+
public ?string $lastName = null;
228+
}
229+
230+
The :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute works on
231+
classes and it can be repeated::
232+
233+
// src/Dto/Source.php
234+
namespace App\Dto;
235+
236+
use App\Dto\B;
237+
use App\Dto\C;
238+
use App\ObjectMapper\TransformNameCallable;
239+
use Symfony\Component\ObjectMapper\Attributes\Map;
240+
241+
#[Map(target: B::class, if: [Source::class, 'shouldMapToB'])]
242+
#[Map(target: C::class, if: [Source::class, 'shouldMapToC'])]
243+
class Source
244+
{
245+
/**
246+
* In case of a condition on a class, $value will be null
247+
*/
248+
public static function shouldMapToB(mixed $value, object $source): bool
249+
{
250+
return false;
251+
}
252+
253+
public static function shouldMapToC(mixed $value, object $source): bool
254+
{
255+
return true;
256+
}
257+
}
258+
259+
Provide mapping as a service
260+
----------------------------
261+
262+
The :class:`Symfony\\Component\\ObjectMapper\\ObjectMapperMetadataFactoryInterface` allows
263+
to change how mapping metadata is computed. With this interface we can create a
264+
`MapStruct`_ version of the Object Mapper::
265+
266+
// src/ObjectMapper/Metadata/MapStructMapperMetadataFactory.php
267+
namespace App\Metadata\ObjectMapper;
268+
269+
use Symfony\Component\ObjectMapper\Attribute\Map;
270+
use Symfony\Component\ObjectMapper\Metadata\ObjectMapperMetadataFactoryInterface;
271+
use Symfony\Component\ObjectMapper\Metadata\Mapping;
272+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
273+
274+
/**
275+
* A Metadata factory that implements the basics behind https://mapstruct.org/.
276+
*/
277+
final class MapStructMapperMetadataFactory implements ObjectMapperMetadataFactoryInterface
278+
{
279+
public function __construct(private readonly string $mapper)
280+
{
281+
if (!is_a($mapper, ObjectMapperInterface::class, true)) {
282+
throw new \RuntimeException(sprintf('Mapper should implement "%s".', ObjectMapperInterface::class));
283+
}
284+
}
285+
286+
public function create(object $object, ?string $property = null, array $context = []): array
287+
{
288+
$refl = new \ReflectionClass($this->mapper);
289+
$mapTo = [];
290+
$source = $property ?? $object::class;
291+
foreach (($property ? $refl->getMethod('map') : $refl)->getAttributes(Map::class) as $mappingAttribute) {
292+
$map = $mappingAttribute->newInstance();
293+
if ($map->source === $source) {
294+
$mapTo[] = new Mapping($map->source, $map->target, $map->if, $map->transform);
295+
296+
continue;
297+
}
298+
}
299+
300+
// Default is to map properties to a property of the same name
301+
if (!$mapTo && $property) {
302+
$mapTo[] = new Mapping($property, $property);
303+
}
304+
305+
return $mapTo;
306+
}
307+
}
308+
309+
With this metadata usage, the mapping definition can be written as a service::
310+
311+
// src/ObjectMapper/AToBMapper
312+
313+
namespace App\Metadata\ObjectMapper;
314+
315+
use App\Dto\Source;
316+
use App\Dto\Target;
317+
use Symfony\Component\ObjectMapper\Attributes\Map;
318+
use Symfony\Component\ObjectMapper\ObjectMapper;
319+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
320+
321+
322+
#[Map(source: Source::class, target: Target::class)]
323+
class AToBMapper implements ObjectMapperInterface
324+
{
325+
public function __construct(private readonly ObjectMapper $objectMapper)
326+
{
327+
}
328+
329+
#[Map(source: 'propertyA', target: 'propertyD')]
330+
#[Map(source: 'propertyB', if: false)]
331+
public function map(object $source, object|string|null $target = null): object
332+
{
333+
return $this->objectMapper->map($source, $target);
334+
}
335+
}
336+
337+
338+
The custom metadata is injected into our :class:`Symfony\\Component\\ObjectMapper\\ObjectMapperInterface`::
339+
340+
$a = new Source('a', 'b', 'c');
341+
$metadata = new MapStructMapperMetadataFactory(AToBMapper::class);
342+
$mapper = new ObjectMapper($metadata);
343+
$aToBMapper = new AToBMapper($mapper);
344+
$b = $aToBMapper->map($a);
345+
346+
.. _`MapStruct`: https://mapstruct.org/

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.