Object Mapper
, (*1)
Index, (*2)
This document is a guide mainly walking you through the setup, concepts, and use
cases of this solution., (*3)
API documentation is bound to code and complies to PHPDoc standards..., (*4)
Secondary sections are collapsed in order to not overflow the first reading., (*5)
Introduction
Use this solution for mapping generically data from source to target object via
extensible strategies and controls., (*6)
Delegate responsibility of all of your data mapping to a generic - extensible -
optimized - tested mapping system., (*7)
Leverage that system to:, (*8)
- Decouple your codebase from data mapping logic
- Dynamically define control flow over data being transferred from source to
target
- Dynamically define target model based on source model and vice-versa
- Easily genericize - centralize - optimize - test - execute data mapping
- Design efficiently and simplify your system
This project aims to provide a standard core system to higher level systems
such as:, (*9)
- ORM
- Form handler
- Serializer
- Data import
- Layers data representation mapper
- ...
If you need to map from/to array
data structure, simply cast it to/from
object
stdClass
., (*10)
Roadmap
To develop this solution faster, contributions are welcome..., (*11)
v1.1.0
- Implement recursion path finder feature
- Implement callable check point feature
- Implement seizing check point feature
- Improve doc
Integrations
Setup
Step 1 - Installation
Open a command console, enter your project directory and execute:, (*12)
$ composer require opportus/object-mapper
Step 2 - Initialization
This library contains 4 services. 3 of them require a single dependency which is
another lower level service among those 4:, (*13)
use Opportus\ObjectMapper\Point\PointFactory;
use Opportus\ObjectMapper\Route\RouteBuilder;
use Opportus\ObjectMapper\Map\MapBuilder;
use Opportus\ObjectMapper\ObjectMapper;
$pointFactory = new PointFactory();
$routeBuilder = new RouteBuilder($pointFactory);
$mapBuilder = new MapBuilder($routeBuilder);
$objectMapper = new ObjectMapper($mapBuilder);
In order for the object mapper to get properly initialized, each of its
services must be instantiated such as above., (*14)
By design, this solution does not provide "helpers" for the instantiation of
its own services which is much better handled the way you're already
instantiating your own services, with a DIC system or whatever., (*15)
Mapping
Overview
In order to transfer data from a source object to a target object, the
ObjectMapper
iterates through each
Route
that it gets from a
Map
,
assigning the value of the current route's source point to this route's
target point., (*16)
Optionally, on the route, check points can be defined in
order to control the value from the source point before it reaches the
target point., (*17)
A route is defined by and composed of its source point, its target point,
and its check points., (*18)
A source point can be either:, (*19)
- A property
- A method
- Any extended type of source point
A target point can be either:, (*20)
- A property
- A method parameter
- Any extended type of target point
A check point can be any implementation of
CheckPointInterface
., (*21)
These routes can be defined automatically via a map's
PathFinderInterface
strategy implementation and/or manually via:, (*22)
Automatic Mapping
Remember that PathFinderInterface
implementations such as those covered next
in this section can get combined., (*23)
Static Path Finder
A basic example of how to automatically map User
's data to UserDto
and
vice-versa:, (*24)
class User
{
private $username;
public function __construct(string $username)
{
$this->username = $username;
}
public function getUsername(): string
{
return $this->username;
}
}
class UserDto
{
public $username;
}
$user = new User('Toto');
$userDto = new UserDto();
// Map the data of the User instance to the UserDto instance
$objectMapper->map($user, $userDto);
echo $userDto->username; // Toto
// Map the data of the UserDto instance to a new User instance
$user = $objectMapper->map($userDto, User::class);
echo $user->getUsername(); // Toto
Calling the ObjectMapper::map()
method passing no $map
argument makes
the method build then use a Map
composed of the default StaticPathFinder
strategy., (*25)
The default StaticPathFinder
strategy determines the appropriate point of the
source class to connect to each point of the target class. Doing so, it
defines a route to follow by the object mapper., (*26)
For the default StaticPathFinder
,
a reference target point can be:, (*27)
The corresponding source point can be:, (*28)
Static Source To Dynamic Target Path Finder
, (*29)
Click for details, (*30)
A basic example of how to automatically map User
's data to DynamicUserDto
:, (*31)
class DynamicUserDto {}
$user = new User('Toto');
$userDto = new DynamicUserDto();
// Build the map
$map = $mapBuilder
->addStaticSourceToDynamicTargetPathFinder()
->getMap();
// Map the data of the User instance to the DynamicUserDto instance
$objectMapper->map($user, $userDto, $map);
echo $userDto->username; // Toto
The default StaticSourceToDynamicTargetPathFinder
strategy determines the
appropriate point of the target object (dynamic point) to connect to
each point of the source class (static point)., (*32)
For the default StaticSourceToDynamicTargetPathFinder
,
a reference source point can be:, (*33)
The corresponding target point can be:, (*34)
- A statically undefined (not existing in class) property having for name the
same as the property source point or
lcfirst(substr($getterSourcePoint, 3))
(PropertyDynamicTargetPoint
)
, (*35)
Dynamic Source To Static Target Path Finder
, (*36)
Click for details, (*37)
A basic example of how to automatically map DynamicUserDto
's data to User
:, (*38)
class DynamicUserDto {}
$userDto = new DynamicUserDto();
$userDto->username = 'Toto';
// Build the map
$map = $mapBuilder
->addDynamicSourceToStaticTargetPathFinder()
->getMap();
// Map the data of the DynamicUserDto instance to a new User instance
$user = $objectMapper->map($userDto, User::class, $map);
echo $user->getUsername(); // Toto
The default DynamicSourceToStaticTargetPathFinder
strategy determines the
appropriate point of the source object (dynamic point) to connect to
each point of the target class (static point)., (*39)
For the default StaticSourceToDynamicTargetPathFinder
,
a reference target point can be:, (*40)
The corresponding source point can be:, (*41)
- A statically undefined (not existing in class) but dynamically defined
(existing in object) property having for name the same as the target point
(
PropertyDynamicSourcePoint
)
, (*42)
Custom Path Finder
The default path finders presented above implement each a specific mapping
logic. In order for those to generically map differently typed objects, they
have to follow a certain convention de facto established by these
path finders. You can map generically differently typed objects only
accordingly to the path finders the map is composed of., (*43)
If the default path finders do not suit your needs, you still can genericize
and encapsulate your domain's mapping logic as subtype(s) of
PathFinderInterface
. Doing so effectively, you leverage Object Mapper to
decouple these objects from your mapping logic... Indeed, when the mapped
objects change, the mapping doesn't., (*44)
For best example of how to implement PathFinderInterface
, refer to the default StaticPathFinder
, StaticSourceToDynamicTargetPathFinder
, and
DynamicSourceToStaticTargetPathFinder
implementations., (*45)
Example, (*46)
class MyPathFinder implements PathFinderInterface
{
private $routeBuilder;
// ...
public function getRoutes(SourceInterface $source, TargetInterface $target): RouteCollection
{
$source->getClassReflection();
$target->getClassReflection();
$routes = [];
/**
* Custom mapping algorithm based on source/target relection and
* possibly their data...
*
* Use route builder to build routes...
*/
return new RouteCollection($routes);
}
}
// Pass to the map builder pathfinders you want it to compose the map of
$map = $mapBuilder
->addStaticPathFinder()
->addPathFinder(new MyPathFinder($routeBuilder))
->getMap();
// Use the map
$user = $objectMapper->map($userDto, User::class, $map);
Manual Mapping
If in your context, such as walked through in the previous
"automatic mapping" section, a mapping strategy is
impossible, you can manually map the source to the target., (*47)
There are multiple ways to define manually the mapping such as introduced in the
2 next sub-sections:, (*48)
Via Map Builder API
, (*49)
Click for details, (*50)
The MapBuilder
is an immutable service which implement a fluent interface., (*51)
A basic example of how to manually map User
's data to ContributorDto
and
vice-versa with the MapBuilder
:, (*52)
class User
{
private $username;
public function __construct(string $username)
{
$this->username = $username;
}
public function getUsername(): string
{
return $this->username;
}
}
class ContributorDto
{
public $name;
}
$user = new User('Toto');
$contributorDto = new ContributorDto();
// Define the route manually
$map = $mapBuilder
->getRouteBuilder()
->setStaticSourcePoint('User::getUsername()')
->setStaticTargetPoint('ContributorDto::$name')
->addRouteToMapBuilder()
->getMapBuilder()
->getMap();
// Map the data of the User instance to the ContributorDto instance
$objectMapper->map($user, $contributorDto, $map);
echo $contributorDto->name; // Toto
// Define the route manually
$map = $mapBuilder
->getRouteBuilder()
->setStaticSourcePoint('ContributorDto::$name')
->setStaticTargetPoint('User::__construct()::$username')
->addRouteToMapBuilder()
->getMapBuilder()
->getMap();
// Map the data of the ContributorDto instance to a new User instance
$user = $objectMapper->map($contributorDto, User::class, $map);
echo $user->getUsername(); // 'Toto'
, (*53)
Via Map Definition Preloading
, (*54)
Click for details, (*55)
Via the map builder API presented above, we define the
map (adding to it routes) on the go. There is another way to define the
map, preloading its definition., (*56)
While this library is designed with map definition preloading in mind, it
does not provide a way to effectively preload a map definition which could be:, (*57)
- Any type of file, commonly used for configuration (XML, YAML, JSON, etc...),
defining statically a map to build at runtime
- Any type of annotation in source and target classes, defining statically
a map to build at runtime
- Any type of PHP routine, defining dynamically a map to build at runtime
- ...
A map being not much more than a collection of routes, you can statically
define it for example by defining its routes FQN this way:, (*58)
map:
- source1::$property=>target1::$property
- source1::$property=>target2::$property
- source2::$property=>target2::$property
Then at runtime, in order to create routes to compose a map of, you can:, (*59)
- Parse your map configuration files, extract from them route definitions
- Parse your source and target annotations, extract from them route
definitions
- Implement any sort of map generator logic outputing route definitions
Then, based on their definitions, build these routes with the initial instance
of the MapBuilder
which will keep and inject them into its built maps which in turn might return
these routes to the object mapper depending on the source and target being
mapped., (*60)
Because an object mapper has a wide range of different use case contexts, this
solution is designed as a minimalist, flexible, and extensible core in order to
get integrated, adapted, and extended seamlessly into any of these contexts.
Therefore, this solution delegates map definition preloading to the
integrating higher level system which can make use contextually of its own DIC,
configuration, and cache systems required for achieving
map definition preloading., (*61)
opportus/object-mapper-bundle
is one system integrating this library (into Symfony 4 application context).
You can refer to it for concrete examples of how to implement
map definition preloading., (*62)
, (*63)
Check Point
A check point, added to a route, allows you to control/transform the value
from the source point before it reaches the target point., (*64)
You can add multiple check points to a route. In this case, these
check points form a chain. The first check point controls the original value
from the source point and returns the value (transformed or not) to the
object mapper. Then, the object mapper passes the value to the next
check point and so on... Until the last check point returns the final value
to be assigned to the target point by the object mapper., (*65)
So it is important to keep in mind that each check point has a unique position
(priority) on a route. The routed value goes through each of the
check points from the lowest to the highest positioned ones such as
represented below:, (*66)
SourcePoint --> $value' --> CheckPoint1 --> $value'' --> CheckPoint2 --> $value''' --> TargetPoint
A simple example implementing CheckPointInterface
, and PathFinderInterface
, to form what we could call a presentation layer:, (*67)
class Contributor
{
private $bio;
public function __construct(string $bio)
{
$this->bio = $bio;
}
public function getBio(): string
{
return $this->bio;
}
}
class ContributorView
{
public $bio;
}
class GenericViewHtmlTagStripper implements CheckPointInterface
{
public function control($value, RouteInterface $route, MapInterface $map, SourceInterface $source, TargetInterface $target)
{
return \strip_tags($value);
}
}
class GenericViewMarkdownTransformer implements CheckPointInterface
{
// ...
public function control($value, RouteInterface $route, MapInterface $map, SourceInterface $source, TargetInterface $target)
{
return $this->markdownParser->transform($value);
}
}
class GenericPresentation extends StaticPathFinder
{
// ...
public function getRoutes(Source $source, Target $target): RouteCollection
{
$routes = parent::getRoutes($source, $target);
$controlledRoutes = [];
foreach ($routes as $route) {
$controlledRoutes[] = $this->routeBuilder
->setSourcePoint($route->getSourcePoint()->getFqn())
->setTargetPoint($route->getTargetPoint()->getFqn())
->addCheckPoint(new GenericViewHtmlTagStripper(), 10)
->addCheckPoint(new GenericViewMarkdownTransformer($this->markdownParser), 20)
->getRoute();
}
return new RouteCollection($controlledRoutes);
}
}
$contributor = new Contributor('');
$map = $mapBuilder
->addPathFinder(new GenericPresentation($markdownTransformer))
->getMap();
$contributorView = $objectMapper->map($contributor, ContributorView::class, $map);
echo $contributorView->bio; // <b>Hello World!</b>
In this example, based on the Object Mapper's abilities, we code a whole
application generic layer with no effort..., (*68)
But what is a layer? Accordingly to
Wikipedia:, (*69)
An abstraction layer is a way of hiding the working details of a subsystem, allowing the separation of concerns to facilitate interoperability and platform independence., (*70)
The more the root system (say an application) has independent layers, the more
it has
data representations,
the more it has to map data from one representation to another., (*71)
Think for example of the Clean Architecture:, (*72)
- Controller maps its (POST) request representation to its corresponding
interactor/usecase request representation
- Interactor maps its usecase request representation to its corresponding
domain entity representation
- Entity gateway maps its domain entity representation to its
corresponding persistence representation, and vice-versa
- Presenter maps its domain entity representation to its corresponding
view representation
Each of these layers' essence is to map data based on the logic they are
composed of. This logic is what we can calle the flow of control over data., (*73)
Referring to our example... This flow of control is defined by the
path finder. These controls are our check points. The ObjectMapper
service is nothing but that concrete layered system. Such layered OOP system is
an object mapper., (*74)
Recursion
A recursion implements CheckPointInterface
.
It is used to recursively map a source point to a target point., (*75)
This means:, (*76)
- During mapping an instance of
A
(that has C
) to B
(that has D
),
mapping in same time C
to D
, AKA simple recursion.
- During mapping an instance of
A
(that has many C
) to B
(that has many D
), mapping in same time many C
to many D
, AKA
in-width recursion or iterable recursion.
- During mapping an instance of
A
(that has C
which has E
) to B
(that has D
which has F
), mapping in same time C
and E
to D
and F
, AKA in-depth recursion.
An example of how to manually map a Post
and its composite objects to its
PostDto
and its composite DTO objects:, (*77)
class Post
{
public Author $author;
public Comment[] $comments;
}
class Author
{
public string $name;
}
class Comment
{
public Author $author;
}
class PostDto {}
class AuthorDto {}
class CommentDto {}
$comment1 = new Comment();
$comment1->author = new Author();
$comment1->author->name = 'clem';
$comment2 = new Comment();
$comment2->author = new Author();
$comment2->author->name = 'bob';
$post = new Post();
$post->author = new Author();
$post->author->name = 'Martin Fowler';
$post->comments = [$comment1, $comment2];
// Let's map the Post instance above and its composites to a new PostDto instance and DTO composites...
$mapBuilder
->getRouteBuilder
->setStaticSourcePoint('Post::$author')
->setDynamicTargetPoint('PostDto::$author')
->addRecursionCheckPoint('Author', 'AuthorDto', 'PostDto::$author') // Mapping also Post's Author to PostDto's AuthorDto
->addRouteToMapBuilder()
->setStaticSourcePoint('Comment::$author')
->setDynamicTargetPoint('CommentDto::$author')
->addRecursionCheckPoint('Author', 'AuthorDto', 'CommentDto::$author') // Mapping also Comment's Author to CommentDto's AuthorDto
->addRouteToMapBuilder()
->setStaticSourcePoint('Post::$comments')
->setDynamicTargetPoint('PostDto::$comments')
->addIterableRecursionCheckPoint('Comment', 'CommentDto', 'PostDto::$comments') // Mapping also Post's Comment's to PostDto's CommentDto's
->addRouteToMapBuilder()
->getMapBuilder()
->addStaticSourceToDynamicTargetPathFinder()
->getMap();
$postDto = $objectMapper->($post, PostDto::class, $map)
get_class($postDto); // PostDto
get_class($postDto->author); // AuthorDto
echo $postDto->author->name; // Matin Fowler
get_class($postDto->comments[0]); // CommentDto
get_class($postDto->comments[0]->author); // AuthorDto
echo $postDto->comments[0]->author->name; // clem
get_class($postDto->comments[1]); // CommentDto
get_class($postDto->comments[1]->author); // AuthorDto
echo $postDto->comments[1]->author->name; // bob
Naturally, all that can get simplified with a higher level PathFinderInterface
implementation defining these recursions automatically based on source and
target point types. These types being hinted in source and target classes
either with PHP or PHPDoc., (*78)
This library may feature such PathFinder
in near future. Meanwhile, you still
can implement yours, and maybe submit it to pull request... :), (*79)