大泽特/data-map

数据结构映射库。

v0.2.0 2019-12-09 21:50 UTC

This package is auto-updated.

Last update: 2024-09-22 19:31:52 UTC


README

数据结构和转换库。

Build Status

定义映射器

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)

获取值并将其转换为布尔值(truefalse01'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 时,常规过滤器将不会调用,但 ifNullifEmptywithNullable 除外。

自定义 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(): 尝试转换为 int
  • float(): 尝试转换为 float
  • bool(): 尝试转换为 bool
  • array(): 尝试转换为 array
  • explode(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: 转换为整数或返回 null
  • float: 转换为浮点数或返回 null
  • bool, boolean: 将值解析为布尔值或返回 null
  • array: 如果可能,将值转换为数组(从数组或可迭代对象)或返回 null
  • explode [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 或返回 null
  • date_format [format="Y-m-d H:i:s"]: 尝试将值转换为日期时间并格式化为字符串,如果无法转换则返回 null
  • date_modify [modifier]: 尝试将值转换为 DateTimeImmutable,然后使用修饰符 $datetime->modify($modifier) 转换它
  • timestamp: 尝试将值转换为日期时间然后转换为时间戳或返回 null
  • json_encode: 将值编码为 JSON 或返回 null
  • json_decode: 从 JSON 字符串中解码数组或失败时返回 null
  • count: 返回数组或 Countable 的计数或当不可计数时返回 null
  • if_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

尝试使用对象公共接口(即)进行对象实例的初始化

  • 通过设置公共属性值
  • 通过使用设置器(setSomethingwithSomething假设不可变性)
// 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()
);

实现InputWrapper以从特定源提取数据

可以明确定义某些对象类型的数据提取。

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"};