Skip to content

Commit 33f1f4b

Browse files
committed
[Object Mapper] component introduction
1 parent cbc2b06 commit 33f1f4b

File tree

1 file changed

+260
-0
lines changed

1 file changed

+260
-0
lines changed

object-mapper.rst

Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
Object Mapping
2+
==============
3+
4+
Symfony provides a mapper to transform a given object to another one.
5+
6+
.. _activating_the_serializer:
7+
8+
Installation
9+
------------
10+
11+
Run this command to install the ``object-mapper`` before using it:
12+
13+
.. code-block:: terminal
14+
15+
$ composer require symfony/object-mapper
16+
17+
Using the ObjectMapper Service
18+
------------------------------
19+
20+
Once enabled, the object mapper service can be injected in any service where
21+
you need it or it can be used in a controller::
22+
23+
// src/Controller/DefaultController.php
24+
namespace App\Controller;
25+
26+
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
27+
use Symfony\Component\HttpFoundation\Response;
28+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
29+
30+
class DefaultController extends AbstractController
31+
{
32+
public function index(ObjectMapperInterface $objectMapper): Response
33+
{
34+
// keep reading for usage examples
35+
}
36+
}
37+
38+
39+
Map an object to another one
40+
----------------------------
41+
42+
To map an object to another one use ``map``::
43+
44+
use App\Dto\Source;
45+
use App\Dto\Target;
46+
47+
$mapper = new ObjectMapper();
48+
$mapper->map(source: new Source(), target: Target::class);
49+
50+
51+
If you alread have a target object, you can use its instance directly::
52+
53+
use App\Dto\Source;
54+
use App\Dto\Target;
55+
56+
$target = new Target();
57+
$mapper = new ObjectMapper();
58+
$mapper->map(source: new Source(), target: $target);
59+
60+
61+
Configure the mapping target using attributes
62+
---------------------------------------------
63+
64+
The Object Mapper component includes a :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute to configure mapping
65+
behavior between objects. Use this attribute on a class to specify the
66+
target class::
67+
68+
// src/Dto/Source.php
69+
namespace App\Dto;
70+
71+
use Symfony\Component\ObjectMapper\Attributes\Map;
72+
73+
#[Map(target: Target::class)]
74+
class Source {}
75+
76+
Configure property mapping
77+
--------------------------
78+
79+
Use the :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute on properties to configure property mapping between
80+
objects. ``target`` changes the target property, ``if`` allows to
81+
conditionnally map properties::
82+
83+
// src/Dto/Source.php
84+
namespace App\Dto;
85+
86+
use Symfony\Component\ObjectMapper\Attributes\Map;
87+
88+
class Source {
89+
#[Map(target: 'fullName')]
90+
public string $firstName;
91+
92+
#[Map(if: false)]
93+
public string $lastName;
94+
}
95+
96+
Transform mapped values
97+
-----------------------
98+
99+
Use ``transform`` to call a function or a
100+
:class:`Symfony\Component\ObjectMapper\CallableInterface`::
101+
102+
// src/ObjectMapper/TransformNameCallable
103+
namespace App\ObjectMapper;
104+
105+
use App\Dto\Source;
106+
use Symfony\Component\ObjectMapper\CallableInterface;
107+
108+
/**
109+
* @implements CallableInterface<Source>
110+
*/
111+
final class TransformNameCallable implements CallableInterface
112+
{
113+
public function __invoke(mixed $value, object $object): mixed
114+
{
115+
return sprintf('%s %s', $object->firstName, $object->lastName);
116+
}
117+
}
118+
119+
// src/Dto/Source.php
120+
namespace App\Dto;
121+
122+
use App\ObjectMapper\TransformNameCallable;
123+
use Symfony\Component\ObjectMapper\Attributes\Map;
124+
125+
class Source {
126+
#[Map(target: 'fullName', transform: TransformNameCallable::class)]
127+
public string $firstName;
128+
129+
#[Map(if: false)]
130+
public string $lastName;
131+
}
132+
133+
134+
The ``if`` and ``transform`` parameters also accept callbacks::
135+
136+
// src/Dto/Source.php
137+
namespace App\Dto;
138+
139+
use App\ObjectMapper\TransformNameCallable;
140+
use Symfony\Component\ObjectMapper\Attributes\Map;
141+
142+
class Source {
143+
#[Map(if: 'boolval', transform: 'ucfirst')]
144+
public ?string $lastName = null;
145+
}
146+
147+
The :class:`Symfony\\Component\\ObjectMapper\\Attribute\\Map` attribute works on
148+
classes and it can be repeated::
149+
150+
// src/Dto/Source.php
151+
namespace App\Dto;
152+
153+
use App\Dto\B;
154+
use App\Dto\C;
155+
use App\ObjectMapper\TransformNameCallable;
156+
use Symfony\Component\ObjectMapper\Attributes\Map;
157+
158+
#[Map(target: B::class, if: [Source::class, 'shouldMapToB'])]
159+
#[Map(target: C::class, if: [Source::class, 'shouldMapToC'])]
160+
class Source {
161+
public static function shouldMapToB(mixed $value, object $object): bool
162+
{
163+
return false;
164+
}
165+
166+
public static function shouldMapToC(mixed $value, object $object): bool
167+
{
168+
return true;
169+
}
170+
}
171+
172+
Provide your own mapping metadata
173+
---------------------------------
174+
175+
The :class:`Symfony\\Component\\ObjectMapper\\MapperMetadataFactoryInterface` allows
176+
to change how mapping metadata is computed. With this interface we can create a
177+
`MapStruct`_ version of the Object Mapper::
178+
179+
// src/ObjectMapper/Metadata/MapStructMapperMetadataFactory.php
180+
namespace App\Metadata\ObjectMapper;
181+
182+
use Symfony\Component\ObjectMapper\Attribute\Map;
183+
use Symfony\Component\ObjectMapper\Metadata\MapperMetadataFactoryInterface;
184+
use Symfony\Component\ObjectMapper\Metadata\Mapping;
185+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
186+
187+
/**
188+
* A Metadata factory that implements the basics behind https://mapstruct.org/.
189+
*/
190+
final class MapStructMapperMetadataFactory implements MapperMetadataFactoryInterface
191+
{
192+
public function __construct(private readonly string $mapper)
193+
{
194+
if (!is_a($mapper, ObjectMapperInterface::class, true)) {
195+
throw new \RuntimeException(sprintf('Mapper should implement "%s".', ObjectMapperInterface::class));
196+
}
197+
}
198+
199+
public function create(object $object, ?string $property = null, array $context = []): array
200+
{
201+
$refl = new \ReflectionClass($this->mapper);
202+
$mapTo = [];
203+
$source = $property ?? $object::class;
204+
foreach (($property ? $refl->getMethod('map') : $refl)->getAttributes(Map::class) as $mappingAttribute) {
205+
$map = $mappingAttribute->newInstance();
206+
if ($map->source === $source) {
207+
$mapTo[] = new Mapping(source: $map->source, target: $map->target, if: $map->if, transform: $map->transform);
208+
209+
continue;
210+
}
211+
}
212+
213+
// Default is to map properties to a property of the same name
214+
if (!$mapTo && $property) {
215+
$mapTo[] = new Mapping(source: $property, target: $property);
216+
}
217+
218+
return $mapTo;
219+
}
220+
}
221+
222+
With this metadata usage, the mapping definition belongs to a mapper class::
223+
224+
// src/ObjectMapper/AToBMapper
225+
226+
namespace App\Metadata\ObjectMapper;
227+
228+
use App\Dto\Source;
229+
use App\Dto\Target;
230+
use Symfony\Component\ObjectMapper\Attributes\Map;
231+
use Symfony\Component\ObjectMapper\ObjectMapper;
232+
use Symfony\Component\ObjectMapper\ObjectMapperInterface;
233+
234+
235+
#[Map(source: Source::class, target: Target::class)]
236+
class AToBMapper implements ObjectMapperInterface
237+
{
238+
public function __construct(private readonly ObjectMapper $objectMapper)
239+
{
240+
}
241+
242+
#[Map(source: 'propertyA', target: 'propertyD')]
243+
#[Map(source: 'propertyB', if: false)]
244+
public function map(object $source, object|string|null $target = null): object
245+
{
246+
return $this->objectMapper->map($source, $target);
247+
}
248+
}
249+
250+
251+
The custom metadata is injected inside our :class:`Symfony\\Component\\ObjectMapper\\ObjectMapperInterface`::
252+
253+
$a = new Source('a', 'b', 'c');
254+
$metadata = new MapStructMapperMetadataFactory(AToBMapper::class);
255+
$mapper = new ObjectMapper($metadata);
256+
$aToBMapper = new AToBMapper($mapper);
257+
$b = $aToBMapper->map($a);
258+
259+
260+
.. _`MapStruct`: https://mapstruct.org/

0 commit comments

Comments
 (0)