大泽特 / data-map
数据结构映射库。
Requires (Dev)
- friends-of-phpspec/phpspec-code-coverage: ^4.3
- phpbench/phpbench: @dev
- phpspec/phpspec: ^6.0
- phpstan/phpstan: ^0.12
This package is auto-updated.
Last update: 2024-09-22 19:31:52 UTC
README
数据结构和转换库。
定义映射器
Mapper
配置是输出结构的描述,定义为关联
[Key1 => Getter1, Key2 => Getter2 ...]
Key
定义输出结构中的属性名,而 Getter
是从输入中提取值的函数。
示例
use DataMap\Getter\GetInteger; use DataMap\Mapper; use DataMap\Input\Input; // Input structure is: $input = [ 'name' => 'John', 'surname' => 'Doe', 'date_birth' => '1970-01-01', 'address' => [ 'street' => 'Foo Street', 'city' => [ 'name' => 'Bar Town', 'country' => 'Neverland', ], ], 'age' => '47', ]; // Required output structore is: $output = [ 'firstName' => 'John', 'fullName' => 'John Doe', 'street' => 'Foo Street', 'city' => 'Bar Town', 'age' => 47, 'birth' => new \DateTimeImmutable('1970-01-01'), ]; // Then mapping definition is: $mapper = new Mapper([ 'firstName' => 'name', // simply get `name` from input and assign to `firstName` property 'fullName' => function (Input $input): string { return $input->get('name') . ' ' . $input->get('surname'); }, // use Closure as Getter function 'street' => 'address.street', // get values from nested structures 'city' => 'address.city.name', 'age' => new GetInteger('age'), // use one of predefined getters 'birth' => new GetDate('date_birth'), // get date as `\DateTimeImmutable` object ]); // Map $input to $output: $output = $mapper->map($input); // Map collection of entries: $outputCollection = array_map($mapper, $inputCollection); // Extend mapper definition: $newMapper = $mapper->withAddedMap(['country' => 'address.city.country']);
获取函数
Getter
通常可以描述为接口
use DataMap\Input\Input; interface Getter { /** * @return mixed */ public function __invoke(Input $input); }
定义映射有两种形式
Getter
可以是字符串,它是new GetRaw('key')
的缩写。Getter
也可以是一个闭包或任何其他可调用对象。它将接收DataMap\Input\Input
作为第一个参数,并将原始输入作为第二个参数。Getter
接口不是必需的,它只是一个提示。
预定义获取器
new GetRaw($key, $default)
通过属性路径获取值,不进行额外转换。
$mapper = new Mapper([ 'name' => new GetRaw('first_name'), // same as: 'name' => 'first_name', ]);
new GetString($key, $default)
获取值并将其转换为字符串(如果可能)或返回 $default
。
$mapper = new Mapper([ 'name' => new GetString('username', 'anonymous'), ]);
new GetInteger($key, $default)
获取值并将其转换为整数(如果可能)或 $default
。
$mapper = new Mapper([ 'age' => new GetInteger('user.age', null), ]);
new GetFloat($key, $default)
获取值并将其转换为浮点数(如果可能)或 $default
。
new GetBoolean($key, $default)
获取值并将其转换为布尔值(true
、false
、0
、1
、'0'
、'1'
)或 $default
。
new GetDate($key, $default)
获取值并将其转换为 \DateTimeImmutable
(如果可能)或 $default
。
new GetJoinedStrings($glue, $key1, $key2, ...)
获取给定键的字符串值并使用 $glue
连接它们。
$mapper = new Mapper([ 'fullname' => new GetJoinedStrings(' ', 'user.name', 'user.surname'), ]);
new GetMappedCollection($key, $callback)
获取给定 $key
的集合并将其映射到 $callback
上,如果条目无法映射则返回 []
。
$characterMapper = new Mapper([ 'fullname' => new GetJoinedStrings(' ', 'name', 'surname'), ] ); $movieMapper = new Mapper([ 'movie' => 'name', 'characters' => new GetMappedCollection('characters', $characterMapper), ]); $mapper->map([ 'name' => 'Lucky Luke', 'characters' => [ ['name' => 'Lucky', 'surname' => 'Luke'], ['name' => 'Joe', 'surname' => 'Dalton'], ['name' => 'William', 'surname' => 'Dalton'], ['name' => 'Jack', 'surname' => 'Dalton'], ['name' => 'Averell', 'surname' => 'Dalton'], ], ]); // result: [ 'movie' => 'Lucky Luke', 'characters' => [ ['fullname' => 'Lucky Luke'], ['fullname' => 'Joe Dalton'], ['fullname' => 'William Dalton'], ['fullname' => 'Jack Dalton'], ['fullname' => 'Averell Dalton'], ], ];
new GetMappedFlatCollection($key, $callback)
类似于 GetMappedCollection
,但结果是扁平化的。
new GetTranslated($key, $map, $default)
获取值并使用提供的关联数组($map
)进行翻译,或者当值没有翻译时返回 $default
。
$mapper = new Mapper([ 'agree' => new GetTranslated('agree', ['yes' => true, 'no' => false], false), ]); $mapper->map(['agree' => 'yes']) === ['agree' => true]; $mapper->map(['agree' => 'no']) === ['agree' => false]; $mapper->map(['agree' => 'maybe']) === ['agree' => false];
GetFiltered::from('key')->...
获取值并通过过滤器管道进行转换。
$mapper = new Mapper([ 'text' => GetFiltered::from('html')->string()->stripTags()->trim()->ifNull('[empty]'), 'time' => GetFiltered::from('datetime')->dateFormat('H:i:s'), 'date' => GetFiltered::from('time_string')->date(), 'amount' => GetFiltered::from('amount_string')->float()->round(2), 'amount_int' => GetFiltered::from('amount_string')->round()->int()->ifNull(0), ]);
使用函数作为过滤器
$greeting = function (string $name): string { return "Hello {$name}!"; }; $mapper = new Mapper([ 'greet' => GetFiltered::from('name')->string()->with($greeting), ]); $mapper->map(['name' => 'John']); // result: ['greet' => 'Hello John!']
当值变为 null
时,常规过滤器将不会调用,但 ifNull
、ifEmpty
和 withNullable
除外。
自定义 null
处理过滤器
$requireInt = function ($value): int { if (!is_int($value)) { throw new InvalidArgumentException('I require int!'); } return $value; }; $mapper = new Mapper([ 'must_be_int' => GetFiltered::from('number')->int()->withNullable($requireInt), ]); $mapper->map(['number' => 'x']); // throws InvalidArgumentException $mapper->map(['number' => 1]); // returns ['required_int' => 1]
GetFiltered
具有一组类似于 FilteredInput
的内置过滤器。
with(callable $filter)
: 将自定义过滤器函数添加到管道中withNullable(callable $filter)
: 将自定义过滤器函数添加到管道中,即使值已变为 null 也会调用string()
: 尝试转换为字符串int()
: 尝试转换为 intfloat()
: 尝试转换为 floatbool()
: 尝试转换为 boolarray()
: 尝试转换为 arrayexplode(string $delimiter)
implode(string $glue)
upper()
lower()
trim()
format(string $template)
: 使用sprintf
模板格式值replace(string $search, string $replace)
stripTags()
numberFormat(int $decimals = 0, string $decimalPoint = '.', string $thousandsSeparator = ',')
round(int $precision = 0)
floor()
ceil()
date()
: 尝试转换为DateTimeImmutable
dateFormat(string $format)
count()
ifNull($default)
ifEmpty($default)
输入抽象
Input
接口定义了从不同数据结构访问数据的通用抽象,因此映射和获取器不应依赖于底层数据类型。
它还允许创建输入装饰器以进行额外的输入处理,如数据过滤、转换、遍历等。
ArrayInput
包装关联数组和 ArrayAccess 对象。
$array = ['one' => 1]; $input = new ArrayInput($array); $input->get('key'); // is translated to: $array['key'] ?? null $input->get('one'); // 1 $input->get('two'); // null $input->get('two', 'default'); // 'default' $input->has('one'); // true $input->has('two'); // false
ObjectInput
包装通用对象,并使用对象公共接口获取数据:公共属性或获取器(不带参数并返回某些值的公共方法)。
键访问方法示例 name
的解析顺序如下
- 检查公共属性
name
- 检查获取器
name()
- 检查获取器
getName()
- 检查获取器
isName()
class Example { public $one = 1; private $two = 2; private $three = 3; public function two(): int { return $this->two; } public function getThree(): int { return $this->three; } } $object = new Example(); $input = new ObjectInput($object); $input->get('one'); // 1 (public property $object->one) $input->get('two'); // 2 (getter $object->()) $input->get('three'); // 3 (getter $object->getThree()) $input->get('four'); // null (no property, no getter) $input->get('four', 'default'); // 'default' $input->has('one'); // true $input->has('four'); // false
RecursiveInput
RecursiveInput
允许使用点表示法($input->get('root.branch.leaf')
)遍历数据树。它装饰 Input
(当前叶)并需要 Wrapper
来包装下一个要访问的叶(可以是数组或对象)。
class Example { public $one = ['nested' => 'nested one']; public function two(): object { return (object)['nested' => 'nested two']; } }; $innerInput = new ObjectInput(new Example()); $input = new RecursiveInput($innerInput, MixedWrapper::default()); $input->get('one'); // ['nested' => 'nested one'] $input->get('one.nested'); // 'nested one' $input->get('one.other'); // null $input->get('two.nested'); // 'nested two' $input->has('one'); // true $input->has('one.nested'); // true $input->has('one.other'); // false
FilteredInput
FilteredInput
是另一个 Input
装饰器,允许在从内部结构提取数据后转换数据。
$innerInput = new ArrayInput([ 'amount' => 123, 'description' => ' string ', 'price' => 123.1234, ]); $input = new FilteredInput($innerInput, InputFilterParser::default()); $input->get('amount | string'); // '123' $input->get('description | trim | upper'); // 'STRING' $input->get('description | integer'); // null $input->get('price | round'); // 123.0 $input->get('description | round'); // null $input->get('price | round 2'); // 123.12 $input->get('price | ceil | integer'); // 124
默认输入解析器支持以下过滤器
string
: 如果可能,将值转换为字符串或返回 null |int
,integer
: 转换为整数或返回 nullfloat
: 转换为浮点数或返回 nullbool
,boolean
: 将值解析为布尔值或返回 nullarray
: 如果可能,将值转换为数组(从数组或可迭代对象)或返回 nullexplode [delimiter=","]
: 使用分隔符(默认为,
)拆分字符串implode [delimiter=","]
: 使用分隔符(默认为,
)连接字符串数组upper
: 转换为字符串大写lower
: 转换为字符串小写trim
,ltrim
,rtrim
: 去除字符串两端的空白字符format
: 使用sprintf
格式化值replace [search] [replace=""]
: 在字符串中替换子字符串,类似于str_replace
函数strip_tags
: 与strip_tags
函数相同number_format [decimals=2] [decimal_point="."] [thousands_separator=","]
: 与number_format
函数相同round [precision=0]
: 与round
函数相同floor
: 向下取整,返回float|null
ceil
: 向上取整,返回float|null
datetime
: 尝试将值转换为DateTimeImmutable
或返回 nulldate_format [format="Y-m-d H:i:s"]
: 尝试将值转换为日期时间并格式化为字符串,如果无法转换则返回 nulldate_modify [modifier]
: 尝试将值转换为DateTimeImmutable
,然后使用修饰符$datetime->modify($modifier)
转换它timestamp
: 尝试将值转换为日期时间然后转换为时间戳或返回 nulljson_encode
: 将值编码为 JSON 或返回 nulljson_decode
: 从 JSON 字符串中解码数组或失败时返回 nullcount
: 返回数组或Countable
的计数或当不可计数时返回 nullif_null [then]
: 当映射的值为 null 时返回默认值if_empty [then]
: 当映射的值为空时返回默认值
示例
- 默认按逗号拆分:
string | explode
- 按自定义字符串拆分:
string | explode "-"
- 默认按逗号连接:
array | implode
- 按自定义字符串连接:
array | implode "-"
- 像
sprintf
一样格式化字符串:string | format "string: %s"
- 从浮点数格式化货币:
float | format "price: $%01.2f"
- 将12.3499
转换为'price: $12.35'
- 转换为字符串并使用默认值:
maybe_string | string | if_null "default"
- 转换为日期并修改:
date_string | date_modify "+1 day"
- 计算映射值的MD5:
key | string | md5
- 将字符串包裹在20个字符之后:
key | string | wordwrap 20
- 使用具有自定义映射值位置的本机函数
key | string | preg_replace "/\s+/" " " $$
函数作为转换
InputFilterParser
的默认配置允许使用任何PHP函数作为转换。默认情况下,映射值作为第一个参数传递给该函数,然后可以传递由过滤器配置定义的其他参数。还可以使用$$
作为占位符来定义映射值的不同的参数位置。
输出格式化
输出映射类型取决于Mapper
使用的Formatter
。
内置格式化器
ArrayFormatter
返回关联数组,这是Mapper转换的原始结果。
$mapper = new Mapper($map); // same as: $mapper = new Mapper($map, new ArrayFormatter());
ObjectConstructor
尝试使用常规构造函数创建新实例。键与构造函数参数通过变量名匹配。
没有值类型和正确性检查,所以当映射类型不匹配时,您将收到TypeError。如果对象构造函数有不在映射中的参数,它也会回退到null
值。
// by class constructor: $mapper = new Mapper($map, new ObjectConstructor(SomeClass::class)); // by static method: $mapper = new Mapper($map, new ObjectConstructor(SomeClass::class, 'method'));
ObjectHydrator
尝试使用对象公共接口(即)进行对象实例的初始化
- 通过设置公共属性值
- 通过使用设置器(
setSomething
或withSomething
假设不可变性)
// hydrate instance clone $mapper = new Mapper($map, new ObjectHydrator(new SomeClass())); // new instance from class name $mapper = new Mapper($map, new ObjectHydrator(SomeClass::class));
定制和扩展
Mapper
由3个组件组成
GetterMap
描述映射为string => Getter
关联Wrapper
将输入混合结构包裹在适当的Input
实现中Formatter
将原始映射结果(关联数组)格式化为数组、对象、XML、JSON等
$mapper = new Mapper($getterMap); // which is equivalent of: $mapper = new Mapper( $getterMap, ArrayFormatter::default(), FilteredWrapper::default() );
实现Input
和Wrapper
以从特定源提取数据
可以明确定义某些对象类型的数据提取。
interface Attributes { public function getAttribute($key, $default = null); } class AttributesInput implements Input { /** @var Attribiutes */ private $attributes; public function get(string $key, $default = null) { return $this->attributes->getAttribute($key, $default); } // ... } class AttributesWrapper implements Wrapper { public function supportedTypes(): array { return [Attributes::class] } public function wrap($data): Input { return new AttributesInput($data); } } $mapper = new Mapper( $getterMap, ArrayFormatter::default(), FilteredWrapper::default()->withWrappers(new AttributesWrapper()) );
为了更好的性能,请仅使用MixedWrapper
默认情况下,Mapper支持嵌套结构获取和值过滤器,这是很好的,但在性能上有所损失(见BENCHMARK.md)。但是,当不需要这些功能时,可以仅创建具有MixedWrapper的Mapper。
$mapper = new Mapper( $getterMap, ArrayFormatter::default(), MixedWrapper::default() );
FilteredInput
的定制过滤器
可以扩展或覆盖过滤器函数列表以使用自己的实现。
$mapper = new Mapper( [ 'slug' => 'title | my_replace "/[\PL]+/u" "-" | trim "-"' ], ArrayFormatter::default(), FilteredWrapper::default()->withFilters([ 'my_replace' => new Filter('preg_replace', ['//', '', '$$']) ]) );
定制Formatter
可以使用自定义格式化器实现比通用对象格式化器更好的对象构造性能。还可以创建创建不同结果类型(如XML、JSON等)的格式化器。
class Person { /** @var string */ private $name; /** @var string */ private $surname; public function __construct(string $name, string $surname) { $this->name = $name; $this->surname = $surname; } // ... } class PersonFormatter implements Formatter { public function format(array $output): Person { return new Person($output['name'], $output['surname']); } } class JsonFormatter implements Formatter { public function format(array $output): string { return json_encode($output); } } $map = [ 'name' => 'person.name | string', 'surname' => 'person.surname | string', ]; $toPerson = new Mapper($map, new PersonFormatter()); $toPerson->map(['person' => ['name' => 'John', 'surname' => 'Doe']]); // result: new Person('John', 'Doe'); $toJson = new Mapper($map, new JsonFormatter()); $toJson->map(['person' => ['name' => 'John', 'surname' => 'Doe']]); // result: {"name":"John","surname":"Doe"};