opportus / object-mapper
通过可扩展的策略和控件将源对象映射到目标对象。
Requires
- php: >=7.4
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpbench/phpbench: ^1.0
- phpunit/phpunit: ^9.5
- dev-master
- v1.0.0
- 1.0.0-beta.13
- v1.0.0-beta.12
- 1.0.0-beta.11
- 1.0.0-beta.10
- 1.0.0-beta.9
- 1.0.0-beta.8
- 1.0.0-beta.7
- 1.0.0-beta.6
- v1.0.0-beta.5
- v1.0.0-beta.4
- v1.0.0-beta.3
- v1.0.0-beta.2
- v1.0.0-beta.1
- v1.0.0-alpha.12
- v1.0.0-alpha.11
- v1.0.0-alpha.10
- v1.0.0-alpha.9
- v1.0.0-alpha.8
- v1.0.0-alpha.7
- v1.0.0-alpha.6
- v1.0.0-alpha.5
- v1.0.0-alpha.4
- v1.0.0-alpha.3
- v1.0.0-alpha.2
- v1.0.0-alpha.1
- dev-property-point-value-phpdoc-type-fix
- dev-php7.1
- dev-recursion-feature
This package is auto-updated.
Last update: 2024-09-15 08:16:01 UTC
README
索引
元数据
本文档主要指导您了解此解决方案的设置、概念和用例。
API 文档与代码绑定,并符合 PHPDoc 标准...
次要部分被折叠,以避免第一次阅读时溢出。
简介
使用此解决方案通过可扩展的策略和控件映射源数据到目标对象。
将所有数据映射的责任委托给一个通用的、可扩展的、优化的、经过测试的映射系统。
利用该系统
- 使代码库与数据映射逻辑解耦
- 动态定义源到目标传输数据的控制流程
- 根据源模型动态定义目标模型,反之亦然
- 轻松通用化、集中化、优化、测试和执行数据映射
- 高效设计并简化系统
本项目旨在为 ORM、表单处理器、序列化器、数据导入、层数据表示映射器等高级系统提供标准核心系统。
- ORM
- 表单处理器
- 序列化器
- 数据导入
- 层数据表示映射器
- ...
如果您需要从/to array
数据结构进行映射,只需将其转换为/from object
stdClass
。
路线图
为了更快地开发此解决方案,欢迎贡献...
v1.1.0
- 实现递归路径查找器功能
- 实现可调用检查点功能
- 实现抓取检查点功能
- 改进文档
集成
- Symfony 4 应用程序 => opportus/object-mapper-bundle
- {{ reference_here_your_own_integration }}
设置
步骤 1 - 安装
打开命令行,进入您的项目目录并执行
$ composer require opportus/object-mapper
步骤 2 - 初始化
此库包含 4 个服务。其中 3 个服务需要单个依赖项,即 4 个服务中的另一个较低级别的服务
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);
为了使 object mapper 正确初始化,必须实例化其每个服务,如下所示。
按设计,此解决方案不提供其实例化服务的“辅助工具”,这比使用 DIC 系统或其他方式实例化自己的服务要好得多。
映射
概述
为了将数据从 源 对象传输到 目标 对象,ObjectMapper
将遍历它从 Map
获得的每个 Route
,将当前 route 的 source point 的值分配给此 route 的 target point。
可选地,在路由上,可以定义检查点以控制源点在到达目标点之前的价值。
路由由其源点、其目标点和其检查点定义和组成。
源点可以是以下任一:
- 属性
- 方法
- 源点的任何扩展类型
目标点可以是以下任一:
- 属性
- 方法参数
- 目标点的任何扩展类型
检查点可以是CheckPointInterface
的任何实现。
这些路由可以通过映射的PathFinderInterface
策略实现自动定义,也可以通过以下方式手动定义:
自动映射
请记住,例如本节接下来介绍的PathFinderInterface
实现可以组合。
静态路径查找器
如何自动将User
的数据映射到UserDto
以及相反的示例
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
调用ObjectMapper::map()
方法时不传递任何$map
参数,将使方法构建并使用由默认的StaticPathFinder
策略组成的Map
。
默认的StaticPathFinder
策略确定将连接到每个目标类点的适当的源类点。这样做,它定义了对象映射器要遵循的路由。
对于默认的StaticPathFinder
,一个引用目标点可以是:
- 公共属性(
PropertyStaticTargetPoint
) - 公共设置器或构造函数的参数(
MethodParameterStaticTargetPoint
)
相应的源点可以是:
- 具有与目标点相同名称的公共属性(
PropertyStaticSourcePoint
) - 具有名称为
'get'.ucfirst($targetPointName)
的公共获取器,且不要求任何参数(MethodStaticSourcePoint
)
静态源到动态目标路径查找器
点击查看详细信息
如何自动将User
的数据映射到DynamicUserDto
的示例
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
默认的StaticSourceToDynamicTargetPathFinder
策略确定将连接到每个源类(静态点)的适当的目标对象(动态点)点。
对于默认的StaticSourceToDynamicTargetPathFinder
,一个引用源点可以是:
- 公共属性(
PropertyStaticSourcePoint
) - 不需要任何参数的公共获取器(
MethodStaticSourcePoint
)
相应的目标点可以是:
- 具有与属性源点相同名称的静态未定义(不在类中存在)属性或
lcfirst(substr($getterSourcePoint, 3))
(PropertyDynamicTargetPoint
)
动态源到静态目标路径查找器
点击查看详细信息
如何自动将DynamicUserDto
的数据映射到User
的示例
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
默认的 DynamicSourceToStaticTargetPathFinder
策略确定将连接到每个 目标 类 的点(静态点)的 源 对象 的适当位置(动态点)。
对于默认的 StaticSourceToDynamicTargetPathFinder
,一个引用的 目标点 可以是:
- 公共属性(
PropertyStaticTargetPoint
) - 公共设置器或构造函数的参数(
MethodParameterStaticTargetPoint
)
相应的源点可以是:
- 一个静态未定义(在类中不存在)但动态定义(在对象中存在)的属性,其名称与 目标点 相同(
PropertyDynamicSourcePoint
)
自定义路径查找器
上述默认的 路径查找器 每个都实现了一种特定的映射逻辑。为了使它们能够通用地映射不同类型的对象,它们必须遵循这些 路径查找器 事实上建立的某种约定。您只能根据 map 所组成的 路径查找器 通用地映射不同类型的对象。
如果默认的 路径查找器 不满足您的需求,您仍然可以将您领域的映射逻辑泛化并封装为 PathFinderInterface
的子类型。这样做实际上利用了 Object Mapper 来解耦这些对象与您的映射逻辑... 事实上,当映射对象更改时,映射不会更改。
关于如何实现 PathFinderInterface
的最佳示例,请参考默认的 StaticPathFinder
、StaticSourceToDynamicTargetPathFinder
和 DynamicSourceToStaticTargetPathFinder
实现。
示例
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);
手动映射
如果在您的上下文中,例如在上一节中提到的 "自动映射" 部分中,映射策略不可能,您可以手动将 源 映射到 目标。
有几种手动定义映射的方法,如下两个子节所述:
通过 Map Builder API
点击查看详细信息
MapBuilder
是一个不可变服务,实现了流畅的接口。
以下是如何使用 MapBuilder
手动将 User
的数据映射到 ContributorDto
以及相反的示例:
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'
通过预加载映射定义
点击查看详细信息
通过上述的 map 构建器 API,我们定义了 map(向其添加 routes)即时。定义 map 的另一种方法是 预加载 其定义。
虽然这个库设计时考虑了 预加载 map 定义,但它不提供一种有效预加载 map 定义 的方法,该定义可以是:
- 任何类型的文件,通常用于配置(XML、YAML、JSON等),定义一个在运行时构建的 map
- 源和目标类中的任何类型的注解,定义一个在运行时构建的 map
- 任何类型的 PHP 程序,定义一个在运行时构建的 map
- ...
由于 map 仅仅是 routes 的集合,您可以通过以下方式静态地定义它,例如定义其 routes FQN
map: - source1::$property=>target1::$property - source1::$property=>target2::$property - source2::$property=>target2::$property
然后,在运行时,为了创建 routes 来组合 map,您可以:
- 解析您的 map 配置文件,从中提取 route 定义
- 解析您的源和目标注解,从中提取路由定义
- 实现任何类型的地图生成逻辑,输出路由定义
然后,基于它们的定义,使用MapBuilder
构建这些路由,这是构建初始实例的,它将保留并将它们注入其构建的映射中,反过来可能会根据映射的源和目标将这些路由返回给对象映射器。
由于对象映射器有广泛的不同使用场景,此解决方案设计为一个极简、灵活和可扩展的核心,以便无缝集成、适应和扩展到这些场景中的任何一个。因此,此解决方案将映射定义预加载委托给集成的高级系统,该系统可以上下文相关地使用其自己的DIC、配置和缓存系统,这些系统对于实现映射定义预加载是必需的。
opportus/object-mapper-bundle是一个集成此库的系统(集成到Symfony 4应用程序上下文中)。您可以参考它来了解如何具体实现映射定义预加载的示例。
检查点
向路由中添加一个检查点,允许您在它到达目标点之前控制/转换来自源点的值。
您可以为路由添加多个检查点。在这种情况下,这些检查点形成一条链。第一个检查点控制来自源点的原始值,并将(转换或未转换)的值返回给对象映射器。然后,对象映射器将值传递给下一个检查点,依此类推...直到最后一个检查点返回由对象映射器分配给目标点的最终值。
因此,重要的是要注意每个检查点在路由上都有一个独特的位置(优先级)。路由的值从最低到最高定位的每个检查点通过,如下所示
SourcePoint --> $value' --> CheckPoint1 --> $value'' --> CheckPoint2 --> $value''' --> TargetPoint
一个简单的示例实现了CheckPointInterface
和PathFinderInterface
,形成了我们所谓的表现层
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('<script>**Hello World!**</script>'); $map = $mapBuilder ->addPathFinder(new GenericPresentation($markdownTransformer)) ->getMap(); $contributorView = $objectMapper->map($contributor, ContributorView::class, $map); echo $contributorView->bio; // <b>Hello World!</b>
在这个例子中,基于对象映射器的能力,我们轻松地编写了一个整个应用程序的通用层...
但什么是层?根据维基百科
抽象层是一种隐藏子系统工作细节的方式,允许分离关注点以促进互操作性和平台独立性。
根系统(例如应用程序)具有越多的独立层,它就有越多的数据表示,它就有越多的将数据从一种表示映射到另一种表示的需要。
例如,考虑一下Clean Architecture
- 控制器将它的(POST)请求表示映射到相应的interactor/usecase请求表示
- Interactor将它的usecase请求表示映射到相应的领域实体表示
- Entity gateway将它的领域实体表示映射到相应的持久化表示,反之亦然
- Presenter将它的领域实体表示映射到相应的视图表示
这些层的本质都是根据它们组成的逻辑来映射数据。这种逻辑我们可以称之为数据的控制流。
参照我们的例子...这个控制流是由路径查找器定义的。这些控制是我们检查点。所谓的ObjectMapper
服务不过就是这样一个具体的分层系统。这样的分层OOP系统就是一个对象映射器。
递归
A 递归实现了CheckPointInterface
。它用于递归地将一个源点映射到一个目标点。
这意味着
- 在将实例
A
(包含C
)映射到B
(包含D
)的同时,将C
映射到D
,称为简单递归。 - 在将实例
A
(包含多个C
)映射到B
(包含多个D
)的同时,将多个C
映射到多个D
,称为宽度递归或可迭代递归。 - 在将实例
A
(包含具有E
的C
)映射到B
(包含具有F
的D
)的同时,将C
和E
映射到D
和F
,称为深度递归。
以下是一个如何手动将Post
及其组合对象映射到其PostDto
及其组合DTO对象的示例
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
自然地,所有这些都可以通过一个更高层次的PathFinderInterface
实现来简化,该实现基于源点和目标点的类型自动定义这些递归。这些类型可以通过PHP或PHPDoc在源和目标类中提示。
这个库可能在未来会具备这样的PathFinder
功能。同时,您仍然可以自己实现,也许可以提交一个拉取请求... :)