通用工具,用于组成转换

1.0.0 2021-11-07 16:51 UTC

This package is auto-updated.

Last update: 2024-09-03 16:42:41 UTC


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\SequenceMorph\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 使用,另一种选择是为 HashUserIdUserTransformer 创建静态配置

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 系统如 GraphQLProtobuf 时。

请参阅反射章节以了解更多信息。

内置转换器

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 类)或可迭代转换的序列,并且传递一个可迭代值(TraversableGenerator 等)时,它将延迟执行,因此不会开始迭代,而是返回一个新的可迭代值,转换将在迭代返回的可迭代值时进行。

每个可迭代值也可以接受数组值,然后使用 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 还可以接受一个名为 propertykey 的参数

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 还可以接受一个名为 propertykey 的命名参数

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() 以将其作为步骤添加。