opportus/object-mapper

通过可扩展的策略和控件将源对象映射到目标对象。


README

License Latest Stable Version Latest Unstable Version Build Codacy Badge Codacy Badge

索引

元数据

本文档主要指导您了解此解决方案的设置、概念和用例。

API 文档与代码绑定,并符合 PHPDoc 标准...

次要部分被折叠,以避免第一次阅读时溢出。

简介

使用此解决方案通过可扩展的策略和控件映射源数据到目标对象。

将所有数据映射的责任委托给一个通用的、可扩展的、优化的、经过测试的映射系统。

利用该系统

  • 使代码库与数据映射逻辑解耦
  • 动态定义源到目标传输数据的控制流程
  • 根据源模型动态定义目标模型,反之亦然
  • 轻松通用化、集中化、优化、测试和执行数据映射
  • 高效设计并简化系统

本项目旨在为 ORM、表单处理器、序列化器、数据导入、层数据表示映射器等高级系统提供标准核心系统。

  • ORM
  • 表单处理器
  • 序列化器
  • 数据导入
  • 层数据表示映射器
  • ...

如果您需要从/to array 数据结构进行映射,只需将其转换为/from object stdClass

路线图

为了更快地开发此解决方案,欢迎贡献...

v1.1.0

  • 实现递归路径查找器功能
  • 实现可调用检查点功能
  • 实现抓取检查点功能
  • 改进文档

集成

设置

步骤 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,将当前 routesource point 的值分配给此 routetarget 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,一个引用目标点可以是:

相应的源点可以是:

静态源到动态目标路径查找器

点击查看详细信息

如何自动将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,一个引用源点可以是:

相应的目标点可以是:

  • 具有与属性源点相同名称的静态未定义(不在类中存在)属性或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,一个引用的 目标点 可以是:

相应的源点可以是:

  • 一个静态未定义(在类中不存在)但动态定义(在对象中存在)的属性,其名称与 目标点 相同(PropertyDynamicSourcePoint

自定义路径查找器

上述默认的 路径查找器 每个都实现了一种特定的映射逻辑。为了使它们能够通用地映射不同类型的对象,它们必须遵循这些 路径查找器 事实上建立的某种约定。您只能根据 map 所组成的 路径查找器 通用地映射不同类型的对象。

如果默认的 路径查找器 不满足您的需求,您仍然可以将您领域的映射逻辑泛化并封装为 PathFinderInterface 的子类型。这样做实际上利用了 Object Mapper 来解耦这些对象与您的映射逻辑... 事实上,当映射对象更改时,映射不会更改。

关于如何实现 PathFinderInterface 的最佳示例,请参考默认的 StaticPathFinderStaticSourceToDynamicTargetPathFinderDynamicSourceToStaticTargetPathFinder 实现。

示例

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

一个简单的示例实现了CheckPointInterfacePathFinderInterface,形成了我们所谓的表现层

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(包含具有EC)映射到B(包含具有FD)的同时,将CE映射到DF,称为深度递归

以下是一个如何手动将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功能。同时,您仍然可以自己实现,也许可以提交一个拉取请求... :)