Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Object Mapper] Add component #20347

Open
wants to merge 4 commits into
base: 7.2
Choose a base branch
from
Open
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
260 changes: 260 additions & 0 deletions object-mapper.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
Object Mapping
==============

Symfony provides a mapper to transform a given object to another one.

.. _activating_the_serializer:

Installation
------------

Run this command to install the ``object-mapper`` before using it:

.. code-block:: terminal

$ composer require symfony/object-mapper

Using the ObjectMapper Service
------------------------------

Once enabled, the object mapper service can be injected in any service where
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once installed ?

because writing enabled can mean that it is not enabled:usable by default after installation

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;

Check failure on line 28 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\ObjectMapperInterface" does not exist

class DefaultController extends AbstractController
{
public function index(ObjectMapperInterface $objectMapper): Response
{
// keep reading for usage examples
}
}


Map an object to another one
----------------------------

To map an object to another one use ``map``::

use App\Dto\Source;
use App\Dto\Target;

$mapper = new ObjectMapper();
$mapper->map(source: new Source(), target: Target::class);


If you alread have a target object, you can use its instance directly::

use App\Dto\Source;
use App\Dto\Target;

$target = new Target();
$mapper = new ObjectMapper();
$mapper->map(source: new Source(), target: $target);


Configure the mapping target using attributes
---------------------------------------------

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;

Check failure on line 71 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Attributes\Map" does not exist

#[Map(target: Target::class)]
class Source {}

Configure property mapping
--------------------------

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;

Check failure on line 86 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Attributes\Map" does not exist

class Source {
#[Map(target: 'fullName')]
public string $firstName;

#[Map(if: false)]
public string $lastName;
}

Transform mapped values
-----------------------

Use ``transform`` to call a function or a
:class:`Symfony\\Component\\ObjectMapper\\CallableInterface`::

Check failure on line 100 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Lint (DOCtor-RST)

Please reorder the use statements alphabetically

// src/ObjectMapper/TransformNameCallable.php
namespace App\ObjectMapper;

use App\Dto\Source;
use Symfony\Component\ObjectMapper\CallableInterface;

Check failure on line 106 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\CallableInterface" does not exist

/**
* @implements CallableInterface<Source>
*/
Comment on lines +108 to +110
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary in a documentation example ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that its best to have a copy/paste that doesn't trigger phpstan errors

final class TransformNameCallable implements CallableInterface
{
public function __invoke(mixed $value, object $object): mixed
{
return sprintf('%s %s', $object->firstName, $object->lastName);
}
}

// src/Dto/Source.php
namespace App\Dto;

use App\ObjectMapper\TransformNameCallable;
use Symfony\Component\ObjectMapper\Attributes\Map;

Check failure on line 123 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Attributes\Map" does not exist

class Source {
#[Map(target: 'fullName', transform: TransformNameCallable::class)]
public string $firstName;

#[Map(if: false)]
public string $lastName;
}


The ``if`` and ``transform`` parameters also accept callbacks::

// src/Dto/Source.php
namespace App\Dto;

use App\ObjectMapper\TransformNameCallable;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This use statement doesn't seem used in this example

soyuka marked this conversation as resolved.
Show resolved Hide resolved
use Symfony\Component\ObjectMapper\Attributes\Map;

Check failure on line 140 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Attributes\Map" does not exist

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;

Check failure on line 156 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Attributes\Map" does not exist

#[Map(target: B::class, if: [Source::class, 'shouldMapToB'])]
#[Map(target: C::class, if: [Source::class, 'shouldMapToC'])]
class Source
{
public static function shouldMapToB(mixed $value, object $object): bool
{
return false;
}

public static function shouldMapToC(mixed $value, object $object): bool
{
return true;
}
}

Provide your own mapping metadata
---------------------------------

The :class:`Symfony\\Component\\ObjectMapper\\MapperMetadataFactoryInterface` 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;

Check failure on line 183 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Attribute\Map" does not exist
use Symfony\Component\ObjectMapper\Metadata\MapperMetadataFactoryInterface;

Check failure on line 184 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Metadata\MapperMetadataFactoryInterface" does not exist
use Symfony\Component\ObjectMapper\Metadata\Mapping;

Check failure on line 185 in object-mapper.rst

View workflow job for this annotation

GitHub Actions / Code Blocks

[Missing class] Class, interface or trait with name "Symfony\Component\ObjectMapper\Metadata\Mapping" does not exist
use Symfony\Component\ObjectMapper\ObjectMapperInterface;

/**
* A Metadata factory that implements the basics behind https://mapstruct.org/.
*/
final class MapStructMapperMetadataFactory implements MapperMetadataFactoryInterface
{
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(source: $map->source, target: $map->target, if: $map->if, transform: $map->transform);

continue;
}
}

// Default is to map properties to a property of the same name
if (!$mapTo && $property) {
$mapTo[] = new Mapping(source: $property, target: $property);
}

return $mapTo;
}
}

With this metadata usage, the mapping definition belongs to a mapper class::

// 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);

soyuka marked this conversation as resolved.
Show resolved Hide resolved
.. _`MapStruct`: https://mapstruct.org/
Loading