kylekatarnls / morph
通用工具,用于组成转换
Requires
- php: ^7.4 || ^8.0
Requires (Dev)
- ext-json: *
- pestphp/pest: ^1.20
README
通用工具,用于组成转换
class User { public int $id; public string $name; public string $email; public string $label; } $user = new User(); $user->id = 42; $user->name = 'Katherine Anderson'; $user->email = 'katherine.anderson@marvelettes.org'; $user->label = ''; $transformer = new Morph\Sequence([ new Morph\PublicPropertiesToArray(), 'array_filter', // or static fn (array $value) => array_filter($value), new Morph\UpperKeysFirstLetter(['id' => 'ID']), // a transformer can be composed with any kind of callable ]); $info = $transformer->transform($user); // or simply: $info = $transformer($user);
$info
是一个数组,如下所示
[
'ID' => 42,
'Name' => 'Katherine Anderson',
'Email' => 'katherine.anderson@marvelettes.org',
]
安装
composer require kylekatarnls/morph
使用
在 Morph
命名空间下所有类都实现了 Morph\Morph
interface Morph { public function transform(); }
并且也是 callable
。
任何类型的 callable
(闭包、函数名、可调用对象)或实现 Morph\Morph
类的实例都可以用作转换(例如,在 Morph\Sequence
、Morph\Merge
等中使用)
class HashUserId implements Morph\Morph { public function transform($value = null, $hashAlgo = null): array { if ($value && $hashAlgo) { return ['userid' => hash((string) $hashAlgo, (string) $value->id)]; } return []; } } $transformer = new Morph\Merge([ static fn ($value) => ['username' => $value->name], new HashUserId(), ]); $user = (object) ['id' => 42, 'name' => 'Georgeanna Tillman']; var_dump($transformer->transform($user, 'sha1')); /* [ 'username' => 'Georgeanna Tillman', 'userid' => '92cfceb39d57d914ed8b14d0e37643de0797ae56', ] */
上面的转换器也可以写成类
class UserTransformer extends Morph\Merge { protected function getTransformers(): array { return [ static fn ($value) => ['username' => $value->name], new HashUserId(), ]; } } $transformer = new UserTransformer(); var_dump($transformer->transform($user, 'sha1'));
Morph\Sequence
可以以相同的方式编写,重写 getTransformers
方法。
请注意,此语法允许懒加载内部转换器,即使未调用 new UserTransformer
,这些转换器也不会被创建。一旦创建,它们仍然会被缓存,所以如果你多次调用 ->transform()
,将重复使用相同的转换器实例。
在上面的示例中,'sha1'
作为 transform()
的附加参数传递,并传递给每个子转换器,而在这种情况下,它只被 HashUserId
使用,另一种选择是为 HashUserId
和 UserTransformer
创建静态配置
class HashUserId extends Morph\MorphBase { private string $hashAlgo; public function __construct(string $hashAlgo) { $this->hashAlgo = $hashAlgo; } public function __invoke($value = null): array { if ($value) { return ['userid' => hash($this->hashAlgo, (string) $value->id)]; } return []; } } class UserTransformer extends Morph\Merge { private string $idHashAlgo; public function __construct(string $idHashAlgo) { $this->idHashAlgo = $idHashAlgo; } protected function getTransformers(): array { return [ static fn ($value) => ['username' => $value->name], new HashUserId($this->idHashAlgo), ]; } } $transformer = new UserTransformer('sha1'); var_dump($transformer->transform($user));
class User implements JsonSerializable { public int $id; public string $name; public string $email; public string $label; public function jsonSerialize(): array { return ModelTransformer::get()->transform($this); } } class ModelTransformer extends Morph\Sequence { private static self $singleton; public static function get(): self { // We can cache the transformer instance // for better performances. // This can be done via a simple singleton // as below. // Or using a container (see Psr\Container\ContainerInterface) // such as the Symfony container. return self::$singleton ??= new self(); } protected function getTransformers(): array { return [ new Morph\PublicPropertiesToArray(), 'array_filter', new Morph\UpperKeysFirstLetter(['id' => 'ID']), ]; } } $user = new User(); $user->id = 42; $user->name = 'Katherine Anderson'; $user->email = 'katherine.anderson@marvelettes.org'; $user->label = ''; echo json_encode($user, JSON_PRETTY_PRINT);
输出
{ "ID": 42, "Name": "Katherine Anderson", "Email": "katherine.anderson@marvelettes.org" }
为什么/何时使用?
虽然如果您只使用 PublicPropertiesToArray
或只有少量简单转换,使用 Morph
可能有些过度,但如果您有复杂的模型或 DTO,并且想要正确隔离输入和输出转换的处理、懒加载它们以及/或在其代码库中共享部分,那么它就变得很有用。
它提供了以清晰的方式表示转换过程或 ETL 系统的步骤。
最后,Morph
包含 Reflection
工具,这可以允许直接在类定义中定义转换,使用属性或 PHPDoc,从而将类与其定义和转换同步。通常,当使用自动记录的 API 系统如 GraphQL 或 Protobuf 时。
请参阅反射章节以了解更多信息。
内置转换器
FilterKeys
过滤数组,仅保留给定可调用函数返回 true
(或为真,如果没有在构造函数中传递可调用函数)的键。
$removePrivateKeys = new \Morph\FilterKeys( static fn (string $key) => $key[0] !== '_', ); $removePrivateKeys([ 'foo' => 'A', '_bar' => 'B', 'biz' => 'C', ]);
[ 'foo' => 'A', 'biz' => 'C', ]
FilterValues
过滤数组,仅保留给定可调用函数返回 true
(或为真,如果没有在构造函数中传递可调用函数)的值。
$removeLowValues = new \Morph\FilterValues( static fn ($value) => $value > 10, ); $removeLowValues([ 'foo' => 12, '_bar' => 14, 'biz' => 7, ]);
[ 'foo' => 12, '_bar' => 14, ]
Getters
返回以 "get"
开头或传递给构造函数的列表或前缀之一的方法列表(作为 \Morph\Reflection\Method
数组)。
class User { public function getName(): string { return 'Bob'; } public function isAdmin(): bool { return false; } public function update(): void {} } $getGetters = new \Morph\Getters(['get', 'is']); $getGetters(User::class);
[ 'Name' => new \Morph\Reflection\Method(new \ReflectionMethod( User::class, 'getName', )), 'Admin' => new \Morph\Reflection\Method(new \ReflectionMethod( User::class, 'isAdmin', )), ]
请注意,Getters
不会调用方法,它只是返回这些方法的定义。
请参阅反射章节以了解更多信息。
GettersToArray
返回对象的每个公共方法(如果该方法以 "get"
开头或传递给构造函数的列表或前缀之一)的值。
class User { public string $id = 'abc'; public function getName(): string { return 'Bob'; } public function isAdmin(): bool { return false; } public function update(): void {} } $bob = new User(); $getGetters = new \Morph\GettersToArray(['get', 'is']); $getGetters($bob);
[ 'Name' => 'Bob', 'Admin' => false, ]
LowerFirstLetter
如果输入是字符串,则将输入的第一个字母转换为小写。如果构造函数中给出映射数组,则在执行小写操作之前将使用该映射数组。
$lowerFirstLetter = new \Morph\LowerFirstLetter([ 'Special' => '***special***', ]); $lowerFirstLetter(5); // 5, non-string input are returned as is $lowerFirstLetter('FooBar'); // "fooBar" $lowerFirstLetter('Special'); // "***special***"
UpperFirstLetter
如果输入是字符串,则将其首字母转换为大写。如果构造函数中提供了映射数组,则在大写操作之前使用此数组。
$upperFirstLetter = new \Morph\UpperFirstLetter([ '***special***' => 'Special', ]); $upperFirstLetter(5); // 5, non-string input are returned as is $upperFirstLetter('fooBar'); // "FooBar" $upperFirstLetter('***special***'); // "Special"
LowerKeysFirstLetter
将给定数组中每个键的首字母转换为小写。如果构造函数中提供了映射数组,则在大写操作之前使用此数组。
$lowerFirstLetter = new \Morph\LowerKeysFirstLetter([ 'Special' => '***special***', ]); $lowerFirstLetter([ 5 => 'abc', 'FooBar' => 'def', 'Special' => 'ghi', ]);
[ 5 => 'abc', 'fooBar' => 'def', '***special***' => 'ghi', ]
UpperKeysFirstLetter
将给定数组中每个键的首字母转换为大写。如果构造函数中提供了映射数组,则在大写操作之前使用此数组。
$upperFirstLetter = new \Morph\UpperKeysFirstLetter([ '***special***' => 'Special', ]); $upperFirstLetter([ 5 => 'abc', 'fooBar' => 'def', '***special***' => 'ghi', ]);
[ 5 => 'abc', 'FooBar' => 'def', 'Special' => 'ghi', ]
Merge
使用 array_merge
合并一系列转换的结果。
$itemsWithTotal = new \Morph\Merge([ static fn ($value) => ['items' => $value], static fn ($value) => ['total' => count($value)], ]); $itemsWithTotal(['A', 'B']);
[ 'items' => ['A', 'B'], 'total' => 2, ]
通常用于组合其他 Morph
类
class User { public string $id = 'abc'; public function getName(): string { return 'Bob'; } public function isAdmin(): bool { return false; } public function update(): void {} } $bob = new User(); $itemsWithTotal = new \Morph\Merge([ new \Morph\PublicPropertiesToArray(), new \Morph\GettersToArray(['get', 'is']), ]); $itemsWithTotal(['A', 'B']);
[ 'id' => 'abc', 'Name' => 'Bob', 'Admin' => false, ]
Only
只保留数组中的给定键。
$info = [ 'firstName' => 'Georgia', 'lastName' => 'Dobbins', 'group' => 'The Marvelettes', ]; $select = new \Morph\Only(['firstName', 'lastName']); $select($info);
[ 'firstName' => 'Georgia', 'lastName' => 'Dobbins', ]
它可以是数组或单个键
$info = [ 'firstName' => 'Georgia', 'lastName' => 'Dobbins', 'group' => 'The Marvelettes', ]; $select = new \Morph\Only('firstName'); $select($info);
[ 'firstName' => 'Georgia', ]
Pick
返回给定键的值或不存在时返回 null。
$info = [ 'firstName' => 'Georgia', 'lastName' => 'Dobbins', 'group' => 'The Marvelettes', ]; $select = new \Morph\Pick('firstName'); $select($info);
'Georgia'
Properties
返回类中定义的属性列表(作为 \Morph\Reflection\Property
数组)
class User { public string $name; protected int $id; private array $cache; } $getProperties = new \Morph\Properties(); $getProperties(User::class);
[ 'name' => new \Morph\Reflection\Property(new \ReflectionProperty( User::class, 'name', )), 'id' => new \Morph\Reflection\Property(new \ReflectionProperty( User::class, 'id', )), 'cache' => new \Morph\Reflection\Property(new \ReflectionProperty( User::class, 'cache', )), ]
请参阅反射章节以了解更多信息。
PublicProperties
返回类中定义的公共属性列表(作为 \Morph\Reflection\Property
数组)
class User { public string $name; protected int $id; private array $cache; } $getPublicProperties = new \Morph\PublicProperties(); $getPublicProperties(User::class);
[ 'name' => new \Morph\Reflection\Property(new \ReflectionProperty( User::class, 'name', )), ]
请参阅反射章节以了解更多信息。
PublicPropertiesToArray
返回类中定义的公共属性列表(作为 \Morph\Reflection\Property
数组)
class User { public string $name; protected int $id; private array $cache; public function __construct(string $name, int $id) { $this->name = $name; $this->id = $id; $this->cache = ['foo' => 'bar']; } } $getPublicValues = new \Morph\PublicPropertiesToArray(); $getPublicValues(new User('Juanita Cowart'));
[ 'name' => 'Juanita Cowart', ]
Sequence
按给定顺序组合转换并执行。每个转换接收前一个转换的结果作为输入。
$data = [ 'singer' => [ 'firstName' => 'Ann', 'lastName' => 'Bogan', ], 'label' => [ 'name' => 'Motown', ], ]; $getSingerLastName = new \Morph\Sequence([ new \Morph\Pick('singer'), new \Morph\Pick('lastName'), ]); $getSingerLastName($data);
'Bogan'
TransformKeys
使用给定的转换转换数组中的每个键。
$data = [ 'first_name' => 'Ann', 'last_name' => 'Bogan', ]; $upperKeys = new \Morph\TransformKeys('mb_strtoupper');
[ 'FIRST_NAME' => 'Ann', 'LAST_NAME' => 'Bogan', ]
TransformValues
使用给定的转换转换数组中的每个值。
$data = [ 'first_name' => 'Ann', 'last_name' => 'Bogan', ]; $upperKeys = new \Morph\TransformValues('mb_strtoupper');
[ 'first_name' => 'ANN', 'last_name' => 'BOGAN', ]
使用给定的转换转换数组中的每个值。
MorphBase
可以扩展以创建新的转换并从便利的方法中继承的抽象 MorphBase
。
class UserTransformer extends \Morph\MorphBase { private $nameTransformer; private $defaultTransformer; public function __construct($nameTransformer, $defaultTransformer) { $this->nameTransformer = $nameTransformer; $this->defaultTransformer = $defaultTransformer; } public function __invoke(User $user): array { $data = $this->mapWithTransformer($this->defaultTransformer, [ 'group' => $user->getGroup(), 'label' => $user->getLabel(), ]; return [ 'name' => $this->useTransformerWith($this->nameTransformer, $user->getName()), ]; } }
useTransformerWith()
可以用作转换任何可调用或具有 transform()
方法的类实例。
mapWithTransformer()
与之相同,但它接受一个数组并将转换应用于此数组的每个值。
Reflection
class ModelDefiner extends \Morph\Sequence { protected function getTransformers(): array { return [ new \Morph\Merge([ new \Morph\PublicProperties(), new \Morph\Getters(['get', 'is']), ]), new \Morph\LowerKeysFirstLetter(), new \Morph\TransformValues(static fn (\Morph\Reflection\Documented $property) => array_filter([ 'type' => $property->getTypeName(), 'description' => $property->getDescription(), ])), ]; } } class User { public int $id; /** * First name(s) / Surname(s). * * Includes middle name(s). */ public string $firstName; /** * Last (family) name(s). */ public string $lastName; /** * Bank account number. */ protected string $bankAccountNumber; /** * Login password. */ private string $password; public function __construct( int $id, string $firstName, string $lastName, string $bankAccountNumber, string $password ) { $this->id = $id; $this->firstName = $firstName; $this->lastName = $lastName; $this->bankAccountNumber = $bankAccountNumber; $this->password = $password; } /** * Complete first and last name. */ public function getName(): string { return $this->firstName . ' ' . $this->lastName; } public function isSafe(): bool { return strlen($this->password) >= 8; } } echo json_encode((new ModelDefiner())(User::class), JSON_PRETTY_PRINT);
输出
{ "id": { "type": "int" }, "firstName": { "type": "string", "description": "First name(s) \/ Surname(s).\n\nIncludes middle name(s)." }, "lastName": { "type": "string", "description": "Last (family) name(s)." }, "name": { "type": "string", "description": "Complete first and last name." }, "safe": { "type": "bool" } }
Iteration
当转换是可迭代的(Morph\Iteration\*Iterable
类)或可迭代转换的序列,并且传递一个可迭代值(Traversable
、Generator
等)时,它将延迟执行,因此不会开始迭代,而是返回一个新的可迭代值,转换将在迭代返回的可迭代值时进行。
每个可迭代值也可以接受数组值,然后使用 array_*
函数以获得更好的性能。
Transformation
Morph\Transformation
是一个构建对象,它允许使用链式操作准备一个具有多个步骤的转换。当想要优化长时间迭代(例如逐行读取大日志文件)的内存消耗时,这很方便。
由于它是一个延迟构建器,因此它不会在调用 ->get()
之前开始任何实际的转换。
function gen() { yield 1; yield 2; yield 3; yield 4; yield 5; yield 6; } var_dump( \Morph\Transformation::take(gen()) ->filter(static fn (int $number) => $number !== 4) ->map(static fn (int $number) => [ 'number' => $number, 'odd' => $number % 2 === 0, ]) ->filter(key: 'odd') ->values() ->array() ->get() );
输出
array(2) {
[0] =>
array(2) {
'number' =>
int(2)
'odd' =>
bool(true)
}
[1] =>
array(2) {
'number' =>
int(6)
'odd' =>
bool(true)
}
}
CountIterable
计数迭代(对 Countable
值使用 count
),否则迭代。
function gen() { yield 1; yield 2; } echo (new \Morph\Iteration\CountIterable())(gen()); // 2
使用 ->count()
在 Transformation
构建器对象上添加它作为步骤。
SumIterable
计数迭代(对 array
值使用 array_sum
),否则迭代。
function gen() { yield 3; yield 2; } echo (new \Morph\Iteration\SumIterable())(gen()); // 5
使用 ->sum()
在 Transformation
构建器对象上添加它作为步骤。
ValuesIterable
获取值(对 array
值使用 array_values
),否则迭代删除索引。
function gen() { yield 'A' => 3; yield 'B' => 2; } foreach ((new \Morph\Iteration\ValuesIterable())(gen()) as $key => $value) { echo "$key: $value\n"; }
由于键被删除,您将获得输出
0: 3
1: 2
使用 ->values()
在 Transformation
构建器对象上添加它作为步骤。
KeysIterable
获取值(对 array
值使用 array_keys
),否则迭代删除输入值,并将索引作为输出值。
function gen() { yield 'A' => 3; yield 'B' => 2; } foreach ((new \Morph\Iteration\ValuesIterable())(gen()) as $key => $value) { echo "$key: $value\n"; }
输出
0: A
1: B
使用 ->keys()
在 Transformation
构建器对象上添加它作为步骤。
FilterIterable
过滤可迭代值,仅保留与给定过滤器匹配的项。
function gen() { yield 'A' => 3; yield 'B' => 2; } foreach ((new \Morph\Iteration\FilterIterable( static fn (int $number) => $number % 2 === 0, ))(gen()) as $key => $value) { echo "$key: $value\n"; }
仅保留与回调匹配的值
B: 2
或者,FilterIterable
还可以接受一个名为 property
或 key
的参数
FilterIterable(property: 'active')
等同于: FilterIterable(static fn ($item) => $item->active ?? false)
FilterIterable(key: 'active')
等价于: FilterIterable(static fn ($item) => $item['active'] ?? false)
此外,在过滤时(在同一个循环中)可以通过设置 dropIndex: true
参数来删除索引。
FilterIterable()
(没有回调、属性或键)将保留真值元素。
在 Transformation
构建器对象上使用 ->filter(...)
以将其作为步骤添加。
FlipIterable
翻转键和值(在 array
值上使用 array_flip
),否则迭代。
function gen() { yield 'A' => 3; yield 'B' => 2; yield 'A' => 5; yield 'B' => 3; } foreach ((new \Morph\Iteration\FlipIterable())(gen()) as $key => $value) { echo "$key: $value\n"; }
输出
3: A
2: B
5: A
3: B
在 Transformation
构建器对象上使用 ->flip()
以将其作为步骤添加。
MapIterable
使用转换回调函数转换可迭代对象的每个值。
回调函数首先接收值,然后是索引,最后是调用转换函数时传递的额外参数。
function gen() { yield 'A' => 3; yield 'B' => 2; } foreach ((new \Morph\Iteration\MapIterable( static fn (int $number) => $number * 2, ))(gen()) as $key => $value) { echo "$key: $value\n"; } foreach ((new \Morph\Iteration\MapIterable( static fn (int $number, string $letter, string $x) => "$letter/$number/$x", ))(gen(), 'x') as $key => $value) { echo "$value\n"; }
输出
A: 6
B: 4
A/6/x
B/4/x
或者,MapIterable
还可以接受一个名为 property
或 key
的命名参数
MapIterable(property: 'active')
等价于: MapIterable(static fn ($item) => $item->active ?? false)
MapIterable(key: 'active')
等价于: MapIterable(static fn ($item) => $item['active'] ?? false)
在 Transformation
构建器对象上使用 ->map(...)
以将其作为步骤添加。
ReduceIterable
迭代地将回调函数应用于数组/可迭代对象的元素,以将其缩减为单个值(在 array
值上使用 array_reduce
),否则迭代。
function gen() { yield 3; yield 2; yield 6; } echo (new \Morph\Iteration\ReduceIterable( static fn ($carry, $item) => $carry * $item, ))(gen(), 1); // 36
初始值可以在构造时或在调用时传递。
在 Transformation
构建器对象上使用 ->reduce()
以将其作为步骤添加。