klimick/decode

以类型安全的方式解码不受信任的数据

0.0.1 2021-06-19 10:46 UTC

This package is not auto-updated.

Last update: 2024-09-23 01:09:11 UTC


README

psalm level psalm type coverage phpunit coverage

这个库允许您取不受信任的数据,并检查它是否可以表示为类型 T

使用示例

<?php

use Klimick\Decode\Decoder as t;

// Describes runtime type for array{name: string, age: int, meta: list<string>}
$libraryDefinition = t\shape(
    id: t\int(),
    name: t\string(),
    meta: t\listOf(t\string()),
);

// Untrusted data
$json = '{
    "id": 42,
    "name": "Decode",
    "meta": [
        "runtime type system",
        "psalm integration",
        "with whsv26/functional"
    ]
}';

// If decode will fail, CastException is thrown.
// $person is array{name: string, age: int, meta: list<string>}
$person = t\tryCast(
    value: $json,
    to: t\fromJson($libraryDefinition),
);

// Either data type from whsv26/functional
// Left side contains decoding errors
// Right side holds decoded valid
// $person is Either<Invalid, Valid<array{name: string, age: int, meta: list<string>}>>
$personEither = t\decode(
    value: $json,
    with: t\fromJson($libraryDefinition),
)

// Option data type from whsv26/functional
// $person is Option<array{name: string, age: int, meta: list<string>}>
$personOption = t\cast(
    value: $json,
    to: t\fromJson($libraryDefinition),
);

内置类型原子操作

mixed()

表示任何可能类型的值。

null()

表示null值的类型。适用于可空类型。

$nullOrInt = union(null(), int())
int()

表示整数。

positiveInt()

表示正整数。

float()

表示带有浮点数的数字。

numeric()

表示整数或浮点数。

numericString()

numeric() 相似,但也可以表示字符串数字。

bool()

表示布尔值。

string()

表示字符串值。

nonEmptyString()

表示不能为空的字符串。

scalar()

任何标量值。

arrKey()

表示数组键(int | string)

datetime()

表示可以创建 DateTimeImmutable 的解码器从字符串。默认情况下,它使用 DateTimeImmutable 构造函数。

您可以指定一个格式,然后解码器将使用 DateTimeImmutable::createFromFormat

$datetime = datetime(fromFormat: 'Y-m-d H:i:s');

默认情况下,它使用UTC时区。您可以在解码器实例化时传递不同的时区。

$datetime = datetime(timezone: 'Moscow/Europe');

泛型类型

union(T1, T2, T3)

表示值可以是多种类型中的一种类型的类型。

// int | string
$intOrString = union(int(), string());
// float | null
$floatOrNull = union(float(), null());
// int | float | string | null
$intOrFloatOrStringOrNull = union($intOrString, $floatOrNull);
arrayOf(TK, TV)

表示具有类型 TK 的键和类型 TV 的值的数组。

// array<int, string>
$arr = arrayOf(int(), string());
nonEmptyArrayOf(TK, TV)

表示具有类型 TK 的键和类型 TV 的值的非空数组。

// non-empty-array<int, string>
$nonEmptyArr = nonEmptyArrayOf(int(), string());
listOf(TV)

表示具有类型 TV 的列表。

// list<string>
$list = listOf(string());
nonEmptyListOf(TV)

表示具有类型 TV 的非空列表。

// non-empty-list<string>
$list = nonEmptyListOf(string());
shape(prop1: T, prop2: T, propN: T)

表示具有已知键的数组。

// array{prop1: int, prop2: string, prop3: bool}
$shape = shape(
    prop1: int(),
    prop2: string(),
    prop3: bool(),
);
partialShape(prop1: T, prop2: T, propN: T)

shape 类似,表示具有已知键的数组,但每个键可能是未定义的。

// array{prop1?: int, prop2?: string, prop3?: bool}
$shape = partialShape(
    prop1: int(),
    prop2: string(),
    prop3: bool(),
);
intersection(T1, T2, T3)

允许将多个 shapepartialShape 合并到一个解码器的解码器。

// array{prop1: string, prop2: string, prop3?: string, prop4?: string}
$intersection = intersection(
    shape(
        prop1: string(),
        prop2: string(),
    ),
    partialShape(
        prop3: string(),
        prop4: string(),
    ),
);
tuple(T1, T2, T3)

表示从零开始索引且具有固定项目计数的数组。

// array{int, string, bool}
$tuple = tuple(int(), string(), bool());
object(SomeClass::class)(prop1: T1, prop2: T2, propN: TN)

允许为已存在的类创建解码器。对于构造函数的每个参数,您必须显式指定相应的解码器。

final class SomeClass
{
    public function __construct(
        public int $prop1,
        public string $prop2,
    ) {}
    
    /**
     * @return DecoderInterface<SomeClass>
     */
    public static function type(): DecoderInterface
    {
        return object(self::class)(
            prop1: int(),
            prop2: string(),
        );
    }
}
partialObject(SomeClass::class)(prop1: T1, prop2: T2, propN: T3)

object 解码器类似,但构造函数的每个参数都必须是可空的。

rec(fn() => T)

表示递归类型。只有对象可以是递归的。

final class SomeClass
{
    /**
     * @param list<SomeClass> $recursive
     */
    public function __construct(
        public int $prop1,
        public string $prop2,
        public array $recursive = [],
    ) { }

    /**
     * @return DecoderInterface<SomeClass>
     */
    public static function type(): DecoderInterface
    {
        $self = rec(fn() => self::type());

        return object(self::class)(
            prop1: int(),
            prop2: string(),
            recursive: listOf($self),
        );
    }
}
fromJson(T)

用于类型 T 的解码器的组合器,该类型将从json表示中解析。

$shapeFromJson = fromJson(
    shape(
        prop1: string(),
        prop2: string(),
    )
);

高阶助手

optional

允许您将属性标记为可能未定义的。

$personD = shape(
    name: string(),
    additional: listOf(string())->optional(),
);

// inferred type: array{name: string, additional?: list<string>}
$firstShape = tryCast(['name' => 'foo'], $personD);

// No additional field
// ['name' => 'foo']
print_r($firstShape);

// inferred type: array{name: string, additional?: list<string>}
$secondShape = tryCast(['name' => 'foo', 'additional' => ['bar']], $personD);

// ['name' => 'foo', 'additional' => ['bar']]
print_r($secondShape);
default

允许您在不受信任的源不提供值时定义一个回退值。

$personD = shape(
    name: string(),
    isEmployed: bool()->default(false),
);

// inferred type: array{name: string, isEmployed: bool}
$firstShape = tryCast(['name' => 'foo'], $personD);

// With default ['isEmployed' => false]
// ['name' => 'foo', 'isEmployed' => false]
print_r($firstShape);

// inferred type: array{name: string, isEmployed: bool}
$secondShape = tryCast(['name' => 'foo', 'isEmployed' => true], $personD);

// ['name' => 'foo', 'isEmployed' => true]
print_r($secondShape);
constrained

所有解码器都可以进行约束。

$personD = shape(
    name: string()->constrained(
        minSize(is: 1),
        maxSize(is: 255),
    ),
    street: string()->constrained(
        minSize(is: 1),
        maxSize(is: 255),
    ),
);

内置约束列表

from

为每个解码器定义了辅助方法 from。它允许您指定结果属性的路径或重命名它。

$personD = shape(
    name: string()->from('$.person'),
    street: string()->from('$.address.street'),
);

$untrustedData = [
    'person' => 'foo',
    'address' => [
        'street' => 'bar',
    ],
];

// Inferred type: array{name: string, street: string}
$personShape = tryCast($untrustedData, $personD);

/* Decoded data looks different rather than source: [
    'name' => 'foo',
    'street' => 'bar',
] */
print_r($personShape);

$ 符号表示对象的根。当您想要更改解码结构嵌套时,可以仅使用 $

$messengerD = shape(
    kind: string()->from('$.messenger_type'),
    contact: string()->from('$.messenger_contact'),
);

$personD = shape(
    name: string()->from('$.person'),
    street: string()->from('$.address.street'),
    messenger: $messengerD->from('$'), // means "use the same data for this decoder"
);

$untrustedData = [
    'person' => 'foo',
    'address' => [
        'street' => 'bar',
    ],
    'messenger_type' => 'telegram',
    'messenger_contact' => '@Klimick',
];

// inferred type: array{name: string, street: string, messenger: array{kind: string, messenger: string}}
$personShape = tryCast($untrustedData, $personD);

/* Decoded data looks different rather than source: [
    'name' => 'foo',
    'street' => 'bar',
    'messenger' => [
        'kind' => 'telegram',
        'contact' => '@Klimick',
    ]
] */
print_r($personShape);

约束

可以使用 constrained 高阶助手将约束附加到解码器上。

equal (所有类型)

检查数值是否等于给定的值。

$fooString = string()
    ->constrained(equal('foo'));
greater (int, float, numeric)

检查一个数值是否大于给定的值。

$greaterThan10 = int()
    ->constrained(greater(10));
greaterOrEqual (int, float, numeric)

检查一个数值是否大于或等于给定的值。

$greaterOrEqualTo10 = int()
    ->constrained(greaterOrEqual(10));
less (int, float, numeric)

检查一个数值是否小于给定的值。

$lessThan10 = int()
    ->constrained(less(10));
lessOrEqual (int, float, numeric)

检查一个数值是否小于或等于给定的值。

$lessOrEqualTo10 = int()
    ->constrained(lessOrEqual(10));
inRange (int, float, numeric)

检查一个数值是否在给定的范围内。

$from10to20 = int()
    ->constrained(inRange(10, 20));
minLength (string, non-empty-string)

检查字符串值的大小不小于给定的值。

$min10char = string()
    ->constrained(minLength(10));
maxLength (string, non-empty-string)

检查字符串值的大小不大于给定的值。

$max10char = string()
    ->constrained(maxLength(10));
startsWith (string, non-empty-string)

检查字符串值是否以给定的值开头。

$startsWithFoo = string()
    ->constrained(startsWith('foo'));
endsWith (string, non-empty-string)

检查字符串值是否以给定的值结尾。

$endsWithFoo = string()
    ->constrained(endsWith('foo'));
uuid (string, non-empty-string)

检查字符串值是否是有效的UUID。

$uuidString = string()
    ->constrained(uuid());
trimmed (string, non-empty-string)

检查字符串值没有前导或尾随空白。

$noLeadingOrTrailingSpaces = string()
    ->constrained(trimmed());
matchesRegex (string, non-empty-string)

检查字符串值是否匹配给定的正则表达式。

$stringWithNumbers = string()
    ->constrained(matchesRegex('/^[0-9]{1,3}$/'));
forall (array)

检查给定的约束对于数组值的所有元素都成立。

$allNumbersGreaterThan10 = forall(greater(than: 10));

$numbersGreaterThan10 = listOf(int())
    ->constrained($allNumbersGreaterThan10);
exists (array)

检查给定的约束对于数组值的一些元素成立。

$hasNumbersGreaterThan10 = exists(greater(than: 10));

$withNumberGreaterThan10 = listOf(int())
    ->constrained($hasNumbersGreaterThan10);
inCollection (array)

检查数组值包含一个与给定值相等的值。

$listWith10 = listOf(int())
    ->constrained(inCollection(10));
maxSize (array)

检查数组值的大小不大于给定的值。

$max10numbers = listOf(int())
    ->constrained(maxSize(is: 10));
minSize (array)

检查数组值的大小不小于给定的值。

$atLeast10numbers = listOf(int())
    ->constrained(minSize(is: 10));
allOf (any type)

所有约束的合取。

$from100to200 = allOf(
    greaterOrEqual(to: 100),
    lessOrEqual(to: 200),
);

$numbersFrom100to200 = listOf(int())
    ->constrained($from100to200);
anyOf (any type)

所有约束的析取。

$from100to200 = allOf(
    greaterOrEqual(to: 100),
    lessOrEqual(to: 200),
);

$from300to400 = allOf(
    greaterOrEqual(to: 300),
    lessOrEqual(to: 400),
);

$numbersFrom100to200orFrom300to400 = listOf(int())
    ->constrained(anyOf($from100to200, $from300to400));