klimick / decode
以类型安全的方式解码不受信任的数据
Requires
- php: ^8.0
- whsv26/functional: ^2.0
Requires (Dev)
- giorgiosironi/eris: ^0.12.0
- klimick/psalm-show-type: ^1.0
- phpunit/phpunit: ^9.5
- psalm/plugin-phpunit: ^0.15.1
- vimeo/psalm: ^4.7
This package is not auto-updated.
Last update: 2024-09-23 01:09:11 UTC
README
这个库允许您取不受信任的数据,并检查它是否可以表示为类型 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)
允许将多个 shape
或 partialShape
合并到一个解码器的解码器。
// 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));