scriptfusion / mapper
使用对象组合领域特定语言(DSL)转换数组。
Requires
- php: ^7.2|^8
- eloquent/enumeration: ^5|^6
- scriptfusion/array-walker: ^1
Requires (Dev)
- mockery/mockery: ^1.3.3
- phpunit/phpunit: ^8.5|^9
- scriptfusion/static-class: ^1
README
Mapper 使用对象组合领域特定语言(DSL)将数组从一种格式转换到另一种格式。应用程序经常从结构不同于所需的外部源接收数据。我们可以使用 Mapper 将外部数据转换为更合适的应用程序格式,如下例所示使用 Mapping
。
$mappedData = (new Mapper)->map($data, new MyMapping);
这假设我们已创建了一个映射,MyMapping
,将 $data
转换为 $mappedData
。
内容
映射
映射是数据转换描述,描述了如何将数据从一种格式转换为另一种格式。映射是一个数组对象包装器,描述了输出格式,使用可以检索和增强输入数据的 表达式。要编写映射,我们必须了解输入数据格式,以便我们可以编写一个表示所需输出格式的数组,并用表达式装饰它以转换输入数据。
示例
在以下简单但人为设计的示例中,我们使用映射将输入数组的键从 foo 重命名为 bar。
$fooData = ['foo' => 123]; class FooToBarMapping extends Mapping { protected function createMapping() { return ['bar' => new Copy('foo')]; } } $barData = (new Mapper)->map($fooData, new FooToBarMapping);
['bar' => 123]
在这个示例中,我们声明了一个映射,FooToBarMapping
,并将其传递给 Mapper::map
方法,将 $fooData
转换为 $barData
。如前所述,这只是一个人为设计的示例,以展示 Mapper 的工作原理;人们可能还想看到一个更实际的示例。
此映射引入了 Copy
策略,该策略从输入数据复制值到输出。策略只是我们可以指定为映射值的表达式类型之一。
表达式
表达式是一个伪类型,表示有效的映射值类型的列表。映射的键永远不会被 Mapper 修改,但其值可能根据表达式类型而更改。以下列出了有效的表达式类型;任何其他类型都会抛出 InvalidExpressionException
。
策略
映射
- 映射片段
- 标量
null
策略 如以下部分所述调用和替换。映射可以包含任意数量的附加嵌入映射或映射片段——映射片段仅是一个由数组而不是 Mapping
对象描述的映射。标量值(整数、浮点数、字符串和布尔值)和 null
没有特殊含义,并在输出中以原样呈现。
编写映射
要编写一个映射,请创建一个新的类,该类扩展 Mapping
并实现其抽象方法 createMapping()
,该方法返回一个策略或一个描述输出格式的数组,其中包含任何组合的表达式。
出于原型设计的目的,我们可以避免编写一个新的映射类,而是创建一个 AnonymousMapping
,将映射定义传递给其构造函数,这比编写一个新类更快。然而,编写映射的最佳方式是编写新类,以便映射具有有意义的名称来识别它们。
建议将映射类命名为 XToYMapping,其中 X 是输入格式的名称,Y 是输出格式的名称。
基于策略的映射
基于策略 的映射是通过在顶层指定策略创建的。通常映射是 基于数组的,尽管这种映射可以包含其他表达式,包括策略,但在顶层它们是一个数组。
一些问题只能通过基于策略的映射来解决。例如,假设我们想要创建一个将两个其他映射组合在顶层的映射。在基于数组的映射中,我们最好的做法如下。
protected function createMapping() { return [ 'foo' => new FooMapping, 'bar' => new BarMapping, ] }
这个映射将 FooMapping
和 BarMapping
组合到我们的映射中,但每个映射将分别映射到新的 foo
和 bar
键下。我们真正想要的是在映射的顶层将每个映射的键组合在一起,但基于数组的映射无法表达这个解决方案。如果我们以 Merge
策略为基础构建我们的映射,我们就可以解决这个问题。
protected function createMapping() { return new Merge(new FooMapping, new BarMapping); }
策略
策略是可以被 Mapper 调用并由其返回值替换的类。策略可以大致分为两类:检索器和增强器。检索策略检索数据,增强器改变其他策略提供的数据。
策略是构建复杂数据操作链的基本构建块,以满足应用程序的特定需求。策略的组合形成了一个强大的对象组合 DSL,允许我们表达如何检索和增强数据以将其塑造成所需的格式。
有关策略的完整列表,请参阅 策略参考。
编写策略
策略必须实现 Strategy
接口,但通常扩展 Delegate
或 Decorator
,因为我们通常编写增强器,这些增强器期望注入其他策略以提供数据。Delegate
和 Decorator
提供了 delegate()
方法,它允许策略使用 Mapper 评估表达式,并且通常需要评估注入的策略。Delegate
可以将任何表达式委派给 Mapper,而 Decorator
只接受 Strategy
对象。
建议使用 Strategy 后缀命名自定义策略,以帮助区分它们和标准策略。
实际示例
假设我们从两个不同的第三方提供商那里接收了两种不同的邮政地址格式。第一个提供商,FooBook,提供单个英国地址。第二个提供商,BarBucket,提供一系列美国地址。我们被要求使用映射将这两种类型转换为应用程序使用的相同统一地址格式。
我们应用程序的地址格式必须是一个包含以下字段的扁平数组。
- line1
- line2(如有适用)
- city
- postcode
- country
FooBook 地址映射
以下是从 FooBook 收到的数据示例。
$fooBookAddress = [ 'address' => [ 'name' => 'Mr A Smith', 'address_line1' => '3 High Street', 'address_line2' => 'Hedge End', 'city' => 'SOUTHAMPTON', 'post_code' => 'SO31 4NG', ], 'country' => 'UK', ];
在继续之前,请尝试自己创建映射,如果不确定要使用哪些策略,请参考 参考。以下代码显示了我们可以如何创建一个映射来将此地址格式转换为应用程序的格式。
class FooBookAddressToAddresesMapping extends Mapping { protected function createMapping() { return [ 'line1' => new Copy('address->address_line1'), 'line2' => new Copy('address->address_line2'), 'city' => new Copy('address->city'), 'postcode' => new Copy('address->post_code'), 'country' => new Copy('country'), ]; } }
由于输入数据已经包含我们想要的数据值,我们只需使用Copy
策略有效地重命名字段即可。我们不需要名称字段,因此它未进行映射。
以下显示了映射输入数据的结果。
$address = (new Mapper)->map($fooBookAddress, new FooBookAddressToAddresesMapping); // Output. [ 'line1' => '3 High Street', 'line2' => 'Hedge End', 'city' => 'SOUTHAMPTON', 'postcode' => 'SO31 4NG', 'country' => 'UK', ]
BarBucket地址映射
以下是从BarBucket接收到的数据的示例。
$barBucketAddress = [ 'Addresses' => [ [ 'Jeremy Martinson, Jr.', '455 Larkspur Dr.', 'Baviera, CA 92908', ], ], ];
此格式与我们的应用程序格式不太相似。特别是,BarBucket的格式支持多个地址,但我们只对映射一个感兴趣,因此我们将假设第一个足够,并丢弃任何其他地址。他们的格式也省略了国家,但我们知道BarBucket只提供美国地址,因此我们可以假设国家总是“US”。请再次尝试在观察以下解决方案之前自己尝试创建映射。
class BarBucketAddressToAddresesMapping extends Mapping { protected function createMapping() { return [ 'line1' => new Copy('Addresses->0->1'), 'city' => new Callback( function (array $data) { return $this->extractCity($data['Addresses'][0][2]); } ), 'postcode' => new Callback( function (array $data) { return $this->extractZipCode($data['Addresses'][0][2]); } ), 'country' => 'US', ]; } private function extractCity($line) { return explode(',', $line, 2)[0]; } private function extractZipCode($line) { if (preg_match('[.*\b(\d{5})]', $line, $matches)) { return $matches[1]; } } }
行1可以直接从输入数据中复制,国家可以使用常量值硬编码,因为我们假设它不会更改。
城市和邮政编码必须从地址的最后一行提取。为此,我们使用指向我们映射的私有方法的Callback
策略。回调之所以是必要的,是因为目前没有任何包含的策略可以执行字符串拆分或正则表达式匹配。
匿名函数包装器选择输入数据的相关部分传递给我们的方法。此解决方案的弱点是引用不存在的值会导致PHP生成未定义索引警告,而注入Copy
策略会在路径的任何部分不存在时优雅地解析为null
。因此,最优雅的解决方案是创建自定义策略以促进代码重用并避免错误,但这超出了本演示的范围。有关更多信息,请参阅编写策略。
以下显示了映射输入数据的结果。
$address = (new Mapper)->map($barBucketAddress, new BarBucketAddressToAddresesMapping); // Output. [ 'line1' => '455 Larkspur Dr.', 'city' => 'Baviera', 'postcode' => '92908', 'country' => 'US', ],
请注意,行2不包括在我们的输出中,因为它在需求中被声明为可选。如果它是必需的,我们只需向我们的映射添加'line2' => null,
即可将值硬编码为null
,因为此提供者从输入数据中永远不会出现。
策略参考
以下策略与Mapper一起提供,提供了一套常用的功能,如下所示。
策略索引
获取器
- Copy – 根据查找路径复制输入数据的一部分或指定数据。
- CopyContext – 复制部分上下文数据。
- CopyKey – 复制当前键。
增强器
- Callback – 使用指定的回调增强数据。
- Collection – 通过对每个数据项应用转换来映射数据集。
- Context – 替换指定表达式的上下文。
- Either – 如果主策略返回非空值,则使用主策略,否则委派到回退表达式。
- Filter – 过滤null值或由指定回调拒绝的值。
- Flatten – 将所有嵌套值移动到顶层。
- IfElse – 根据指定条件严格评估为true与否委派到一个表达式或另一个。
- IfExists – 根据指定条件映射到null与否委派到一个表达式或另一个。
- Join – 使用粘合字符串将子字符串表达式连接在一起。
- 合并 – 合并两个数据集,当键冲突时优先考虑后者。
- 替换 – 替换一个或多个子字符串。
- 取第一个 – 从集合中取出第一个值一次或多次。
- 转换为列表 – 将数据转换为单元素列表,除非它已经是一个列表。
- 尝试捕获 – 尝试主要策略,如果抛出异常则回退到表达式。
- 类型 – 将数据转换为指定的类型。
- 唯一 – 通过删除重复项创建一个唯一值的集合。
其他
- 调试 – 通过在插入此策略的地方断开调试器来调试映射。
复制
根据查找路径复制输入数据的一部分或指定数据。支持遍历嵌套数组。默认情况下使用当前记录作为数据源,但如果指定了 data 参数,则使用它。
Copy
可能是最常用的策略,无论是独立使用还是注入到其他策略中。由于其 path 和 data 参数都可以是映射表达式,因此它非常灵活,可以与其他策略或甚至与自身组合,以产生强大的转换。
签名
Copy(Strategy|Mapping|array|mixed $path, Strategy|Mapping|array|mixed $data)
$path
– 路径组件数组,由->
分隔的路径组件字符串或解析为这种表达式的策略或映射。$data
– 可选。要复制的数组数据或解析为数组的表达式,而不是输入数据。
示例
$data = [ 'foo' => [ 'bar' => 123, ], ]; (new Mapper)->map($data, new Copy('foo'));
['bar' => 123]
(new Mapper)->map($data, new Copy('foo->bar')); // or (new Mapper)->map($data, new Copy(['foo', 'bar']));
123
数据覆盖示例
当在第二个参数中指定数据时,将使用该数据而不是 Mapper
发送的数据。
(new Mapper)->map( ['foo' => 'bar'], new Copy('foo', ['foo' => 'baz']) );
'baz'
递归路径解析器示例
由于路径可以来自其他策略,我们可以嵌套 Copy
实例以查找其他键引用的值。
(new Mapper)->map( [ 'foo' => 'bar', 'bar' => 'baz', 'baz' => 'qux', ], new Copy(new Copy(new Copy('foo'))) );
'qux'
复制上下文
复制上下文数据的一部分;在所有其他方面与 Copy
的工作方式完全相同。
签名
CopyContext(Strategy|Mapping|array|mixed $path)
$path
– 路径组件数组,由->
分隔的路径组件字符串或解析为这种表达式的策略或映射。
示例
$data = ['foo' => 123]; $context = ['foo' => 456]; (new Mapper)->map($data, new CopyContext('foo'), $context);
456
复制键
复制当前键从键上下文。默认情况下键上下文为 null
。键上下文可以通过 CollectionMapper
或 集合 策略设置。
签名
CopyKey()
示例
(new Mapper)->map( [ 'foo' => [ 'bar' => 'baz', ], ], new Collection( new Copy('foo'), new CopyKey ) )
['bar' => 'bar']
回调
使用指定的回调函数的返回值增强数据。
建议仅用于原型设计,如果传递闭包并在稍后将其转换为策略,但是可以使用方法指针使用此策略。这是因为策略和方法都有名称,而闭包是匿名的。通常首选策略,因为它们是可重用的。
签名
Callback(callable $callback)
$callback
– 回调函数,它接收映射数据作为其第一个参数,上下文作为其第二个参数。
示例
(new Mapper)->map( range(1, 5), new Callback( function ($data) { $total = 0; foreach ($data as $number) { $total += $number; } return $total; } ) );
15
集合
通过使用回调对每个数据项应用转换来映射数据集合。数据集合必须是一个映射到数组的表达式,否则返回 null。
对于集合中的每个项目,此策略将上下文设置为当前数据项,将键上下文设置为当前键,这可以通过使用 CopyKey 获取。
签名
Collection(Strategy|Mapping|array|mixed $collection, Strategy|Mapping|array|mixed $transformation)
$collection
– 映射到数组的表达式。$transformation
– 转换表达式。当前数据项作为上下文传递。
示例
(new Mapper)->map( ['foo' => range(1, 5)], new Collection( new Copy('foo'), new Callback( function ($data, $context) { return $context * 2; } ) ) );
[2, 4, 6, 8, 10]
上下文
替换指定表达式的上下文。
签名
Context(Strategy|Mapping|array|mixed $expression, Strategy|Mapping|array|mixed $context)
$expression
– 表达式。$context
– 新上下文。
示例
(new Mapper)->map( ['foo' => 123], new Context( new CopyContext('foo'), ['foo' => 456] ), ['foo' => 789] );
456
否则
如果主要策略返回非空值,则使用主要策略,否则委托给回退表达式。
签名
Either(Strategy $strategy, Strategy|Mapping|array|mixed $expression)
$strategy
– 主要策略。$expression
– 回退表达式。
示例
(new Mapper)->map( ['bar' => 'bar'], new Either(new Copy('foo'), new Copy('bar')) );
'bar'
过滤
过滤空值或由指定回调函数拒绝的值。
签名
Filter(Strategy|Mapping|array|mixed $expression, callable $callback = null)
$expression
– 表达式。$callback
– 接收当前值作为其第一个参数、当前键作为其第二个参数和上下文作为其第三个参数的回调函数。
示例
(new Mapper)->map( ['foo' => range(1, 10)], new Filter( new Copy('foo'), function ($value) { return $value % 2; } ) );
[1, 3, 5, 7, 9]
展开
将所有嵌套值移动到顶层。
签名
Flatten(Strategy|Mapping|array|mixed $expression)
$expression
– 表达式。
方法
ignoreKeys($ignore = true)
– 当为true时,在合并时仅考虑值,否则重复键由最后一个访问的键替换,具有优先权。默认为false以保留键。
示例
$data = [ 'foo' => [ range(1, 3), 'bar' => [range(3, 5)], ], ]; (new Mapper)->map($data, new Flatten(new Copy('foo')));
[3, 4, 5]
(new Mapper)->map($data, (new Flatten(new Copy('foo')))->ignoreKeys());
[1, 2, 3, 3, 4, 5]
如果否则
根据指定的条件是否严格评估为true,将委托给一个表达式或另一个表达式。
如果条件不返回布尔值,将抛出InvalidConditionException
。
签名
IfElse(callable $condition, Strategy|Mapping|array|mixed $if, Strategy|Mapping|array|mixed $else = null)
$condition
– 条件。$if
– 当条件评估为true时使用的表达式。$else
– 当条件评估为false时使用的表达式。
示例
(new Mapper)->map( ['foo' => 'foo'], new IfElse( function ($data) { return $data['foo'] !== 'bar'; }, true, false ) );
true
如果存在
根据指定的条件是否映射到null,将委托给一个表达式或另一个表达式。
签名
IfExists(Strategy $condition, Strategy|Mapping|array|mixed $if, Strategy|Mapping|array|mixed $else = null)
$condition
– 条件。$if
– 当条件映射到非null时使用的表达式。$else
– 当条件映射到null时使用的表达式。
示例
$data = ['foo' => 'foo']; (new Mapper)->map($data, new IfExists(new Copy('foo'), true, false));
true
(new Mapper)->map($data, new IfExists(new Copy('bar'), true, false));
false
连接
使用粘合字符串将表达式组合在一起。
签名
Join(string $glue, array ...$expressions)
$glue
– 粘合剂。$expressions
– 要连接的表达式或解析为数组以连接的单个表达式。
示例
(new Mapper)->map( ['foo' => 'foo'], new Join('-', new Copy('foo'), 'bar') );
'foo-bar'
(new Mapper)->map( ['foo' => ['bar', 'baz']], new Join('-', new Copy('foo')) );
'bar-baz'
合并
合并两个数据集,如果字符串键冲突,则优先考虑后者;整数键永远不会冲突。更多信息请参见 array_merge。
签名
Merge(Strategy|Mapping|array|mixed $first, Strategy|Mapping|array|mixed $second)
$first
– 第一个数据集。$second
– 第二个数据集。
示例
(new Mapper)->map( [ 'foo' => range(1, 3), 'bar' => range(3, 5), ], new Merge(new Copy('foo'), new Copy('bar')) );
[1, 2, 3, 3, 4, 5]
替换
替换所有出现的一个或多个子字符串。
可以指定任意数量的搜索和替换。搜索和替换按对解析。如果没有指定替换,则将删除所有匹配项而不是替换。如果指定的替换少于搜索,则最后一个替换将用于剩余的搜索。如果指定的替换多于搜索,则忽略额外的替换。
搜索可以指定为字符串字面量或用Expression
包装,并作为正则表达式处理。可以混合使用Expression
和字符串搜索。正则表达式替换可以引用子匹配,例如$1
指定第一个捕获组。
签名
Replace(Strategy|Mapping|array|mixed $expression, string|Expression|array $searches, string|string[]|null $replacements)
$expression
– 要搜索的表达式。$searches
– 搜索字符串。$replacements
– 可选。替换字符串。
示例
(new Mapper)->map( ['Hello World'], new Replace( new Copy(0), ['Hello', new Expression('[\h*world$]i')], ['こんにちは', '世界'] ) )
'こんにちは世界'
取第一个
根据指定的深度从集合中取第一次值一个或多个次。如果深度超过集合的嵌套层级数,则返回最后遇到的项目。
签名
TakeFirst(Strategy|Mapping|array|mixed $collection, int $depth = 1)
$collection
– 映射到数组的表达式。$depth
– 下降到嵌套集合的次数。
示例
(new Mapper)->map( [ 'foo' => [ 'bar' => [ 'baz' => 123, 'quz' => 456, ], ], ], new TakeFirst(new Copy('foo'), 2) );
123
转换为列表
将数据转换为单个元素列表,除非它已经是列表。列表被定义为具有连续整数键的数组。
这是由于某些格式将单值列表表示为裸值而不是仅包含该值的列表。此策略确保表达式始终是列表,如果它不是列表,则将其包装在数组中。
签名
ToList(Strategy|Mapping|array|mixed $expression)
$expression
– 表达式。
示例
(new Mapper)->map(['foo' => 'bar'], new ToList(new Copy('foo')));
['bar']
尝试捕获
尝试主要策略,如果抛出异常,则回退到表达式。抛出的异常传递给指定的异常处理器。如果处理器不期望接收到的异常类型,则处理器应该抛出异常。
通过嵌套多个此策略的实例,可以为不同的异常类型使用不同的回退表达式。
签名
TryCatch(Strategy $strategy, callable $handler, Strategy|Mapping|array|mixed $expression)
$strategy
– 主要策略。$handler
– 接收抛出的异常作为其第一个参数和数据作为其第二个参数的异常处理器。$expression
– 回退表达式。
示例
(new Mapper)->map( ['foo' => 'bar'], new TryCatch( new Callback( function () { throw new \DomainException; } ), function (\Exception $exception, array $data) { if (!$exception instanceof \DomainException) { throw $exception; } }, new Copy('foo') ) );
'bar'
类型
将数据强制转换为指定的类型。
签名
Type(DataType $type, Strategy $strategy)
$type
– 要转换的类型。$stategy
– 策略。
示例
(new Mapper)->map(['foo' => 123], new Type(DataType::STRING(), new Copy('foo')));
'123'
唯一
通过去除重复项创建一个唯一值集合。
签名
Unique(Strategy|Mapping|array|mixed $collection)
$collection
– 映射到数组的表达式。
示例
(new Mapper)->map( ['foo' => array_merge(range(1, 3), range(3, 5))], new Unique(new Copy('foo')) );
[1, 2, 3, 4, 5]
调试
通过在策略插入位置中断调试器来调试映射。指定的表达式将在触发断点之前立即映射。调试器应该看到当前数据、上下文和映射表达式。
目前仅支持Xdebug调试器。
签名
Debug(Strategy|Mapping|array|mixed $expression)
$expression
– 要委托给Mapper
的表达式。
要求
限制
- 策略不知道它们分配到的键的名称,因为
Mapper
没有转发键名称。 - 策略不知道它们在
Mapping
中的位置,因此不能相对于它们的位置遍历映射。 Collection
策略会覆盖上下文,使任何之前上下文对后代不可访问。
测试
Mapper已经完全单元测试。使用composer test
命令运行测试。本文件中的所有示例都可以在DocumentationTest
中找到。