nette/schema

📐 Nette Schema:根据给定的模式验证数据结构。

v1.3.0 2023-12-11 11:54 UTC

README

Nette Schema

Downloads this Month Tests Coverage Status Latest Stable Version License

简介

一个用于根据给定模式验证和规范数据结构的实用库,具有智能且易于理解的API。

文档可以在网站上找到。

安装

composer require nette/schema

需要PHP版本8.1,并支持PHP 8.1至8.4。

支持我

你喜欢Nette Schema吗?你期待新功能吗?

Buy me a coffee

谢谢!

基本用法

在变量 $schema 中我们有一个验证模式(我们稍后会解释这是什么以及如何创建它),在变量 $data 中我们有一个我们想要验证和规范化的数据结构。这可以是,例如,用户通过API发送的数据,配置文件等。

这个任务由 Nette\Schema\Processor 类处理,它处理输入并返回规范化的数据,或者在出错时抛出 Nette\Schema\ValidationException 异常。

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'Data is invalid: ' . $e->getMessage();
}

方法 $e->getMessages() 返回所有消息字符串的数组,而 $e->getMessageObjects() 返回所有消息作为 Nette\Schema\Message 对象。

定义模式

现在让我们创建一个模式。我们使用类 Nette\Schema\Expect 来定义它,我们实际上定义了对数据应该是什么样的期望。假设输入数据必须是一个包含元素 processRefund(布尔类型)和 refundAmount(整型)的结构(例如数组)。

use Nette\Schema\Expect;

$schema = Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
]);

我们认为模式定义看起来很清晰,即使你第一次看到它。

让我们发送以下数据进行验证

$data = [
	'processRefund' => true,
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // OK, it passes

输出,即 $normalized 的值,是 stdClass 对象。如果我们想输出是一个数组,我们向模式添加一个类型转换 Expect::structure([...])->castTo('array')

结构中的所有元素都是可选的,默认值为 null。示例

$data = [
	'refundAmount' => 17,
];

$normalized = $processor->process($schema, $data); // OK, it passes
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

默认值是 null 并不意味着它会接受在输入数据中 'processRefund' => null。不,输入必须是布尔值,即只有 truefalse。我们必须通过 Expect::bool()->nullable() 显式允许 null

可以使用 Expect::bool()->required() 将项目变为必需的。我们使用 Expect::bool()->default(false) 或简短地使用 Expect::bool(false) 将默认值更改为 false

如果我们想接受布尔值之外的 10,我们列出允许的值,这些值也会被规范化为布尔值

$schema = Expect::structure([
	'processRefund' => Expect::anyOf(true, false, 1, 0)->castTo('bool'),
	'refundAmount' => Expect::int(),
]);

$normalized = $processor->process($schema, $data);
is_bool($normalized->processRefund); // true

现在你了解了模式定义的基础以及结构中各个元素的行为。我们现在将展示在定义模式时可以使用所有其他元素。

数据类型:type()

在模式中可以列出所有标准的PHP数据类型

Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])

然后所有通过Expect::type('scalar')或简写形式Expect::scalar()支持的类型,都可以由 Validators 支持。也可以接受类或接口名称,例如Expect::type('AddressEntity')

您还可以使用联合符号

Expect::type('bool|string|array')

默认值总是null,除了对于arraylist,它们的默认值是一个空数组。(列表是一个按数字键升序索引的数组,即非关联数组)。

值数组: arrayOf() listOf()

数组结构太通用,更有用的是指定它可以包含的确切元素。例如,只能包含字符串的数组

$schema = Expect::arrayOf('string');

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERROR: 123 is not a string

第二个参数可以用来指定键(自版本 1.2 开始)

$schema = Expect::arrayOf('string', 'int');

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: 'a' is not int

列表是一个索引数组

$schema = Expect::listOf('string');

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERROR: 123 is not a string
$processor->process($schema, ['key' => 'a']); // ERROR: is not a list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: is not a list

参数也可以是一个模式,因此我们可以写

Expect::arrayOf(Expect::bool())

默认值是一个空数组。如果您指定了一个默认值并调用了mergeDefaults(),它将与传递的数据合并。

枚举: anyOf()

anyOf()是一组值或模式,一个值可以是。以下是如何编写一个可以包含'a'truenull元素的数组

$schema = Expect::listOf(
	Expect::anyOf('a', true, null),
);

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ERROR: false does not belong there

枚举元素也可以是模式

$schema = Expect::listOf(
	Expect::anyOf(Expect::string(), true, null),
);

$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ERROR

anyOf()方法接受变体作为单独的参数,而不是作为数组。要传递一个值数组,请使用展开运算符anyOf(...$variants)

默认值是null。使用firstIsDefault()方法将第一个元素设置为默认值

// default is 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

结构体

结构体是具有定义键的对象。这些键 => 值对中的每一个都被称为“属性”

结构体接受数组和对象,并返回stdClass对象(除非您使用castTo('array')等更改它)。

默认情况下,所有属性都是可选的,并具有默认值null。您可以使用required()定义必需属性

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // the default value is null
]);

$processor->process($schema, ['optional' => '']);
// ERROR: option 'required' is missing

$processor->process($schema, ['required' => 'foo']);
// OK, returns {'required' => 'foo', 'optional' => null}

如果您不想输出仅具有默认值的属性,请使用skipDefaults()

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(),
])->skipDefaults();

$processor->process($schema, ['required' => 'foo']);
// OK, returns {'required' => 'foo'}

尽管null是可选属性的默认值,但它不允许在输入数据中(值必须是一个字符串)。接受null的属性使用nullable()定义

$schema = Expect::structure([
	'optional' => Expect::string(),
	'nullable' => Expect::string()->nullable(),
]);

$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' expects to be string, null given.

$processor->process($schema, ['nullable' => null]);
// OK, returns {'optional' => null, 'nullable' => null}

默认情况下,输入数据中不允许有额外项

$schema = Expect::structure([
	'key' => Expect::string(),
]);

$processor->process($schema, ['additional' => 1]);
// ERROR: Unexpected item 'additional'

我们可以使用otherItems()来更改这一点。作为参数,我们将指定每个额外元素的模式

$schema = Expect::structure([
	'key' => Expect::string(),
])->otherItems(Expect::int());

$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ERROR

弃用

您可以使用deprecated([string $message])方法弃用属性。弃用通知由$processor->getWarnings()返回

$schema = Expect::structure([
	'old' => Expect::int()->deprecated('The item %path% is deprecated'),
]);

$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["The item 'old' is deprecated"]

范围:min() max()

使用min()max()限制数组的元素数量

// array, at least 10 items, maximum 20 items
Expect::array()->min(10)->max(20);

对于字符串,限制它们的长度

// string, at least 10 characters long, maximum 20 characters
Expect::string()->min(10)->max(20);

对于数字,限制它们的值

// integer, between 10 and 20 inclusive
Expect::int()->min(10)->max(20);

当然,您只需提及min()或只提及max()

// string, maximum 20 characters
Expect::string()->max(20);

正则表达式:pattern()

使用pattern(),您可以指定一个必须匹配整个输入字符串的正则表达式(即,就像它被字符^$包裹一样)

// just 9 digits
Expect::string()->pattern('\d{9}');

自定义断言:assert()

您可以使用assert(callable $fn)添加任何其他限制。

$countIsEven = fn($v) => count($v) % 2 === 0;

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // the count must be even

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 is not even

或者

Expect::string()->assert('is_file'); // the file must exist

您可以为每个断言添加自己的描述。它将成为错误消息的一部分。

$schema = Expect::arrayOf('string')
	->assert($countIsEven, 'Even items in array');

$processor->process($schema, ['a', 'b', 'c']);
// Failed assertion "Even items in array" for item with value array.

该方法可以重复调用以添加多个约束。它可以与对transform()castTo()的调用混合使用。

转换:transform()

可以使用自定义函数修改成功验证的数据

// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));

该方法可以重复调用以添加多个转换。它可以与对assert()castTo()的调用混合使用。操作将按它们声明的顺序执行

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'All characters must be lowercased')
	->transform(fn(string $s) => strtoupper($s)); // conversion to uppercase

transform() 方法可以同时进行转换和验证值。这通常比链式调用 transform()assert() 更简单、更少冗余。为此,该函数接收一个带有 addError() 方法的 Nette\Schema\Context 对象,可以用来添加关于验证问题的信息

Expect::string()
	->transform(function (string $s, Nette\Schema\Context $context) {
		if (!ctype_lower($s)) {
			$context->addError('All characters must be lowercased', 'my.case.error');
			return null;
		}

		return strtoupper($s);
	});

类型转换:castTo()

成功验证的数据可以被转换

Expect::scalar()->castTo('string');

除了原生 PHP 类型外,还可以转换为类。它区分是否有构造函数的简单类或带构造函数的类。如果类没有构造函数,会创建一个其实例,并将结构体中的所有元素写入其属性

class Info
{
	public bool $processRefund;
	public int $refundAmount;
}

Expect::structure([
	'processRefund' => Expect::bool(),
	'refundAmount' => Expect::int(),
])->castTo(Info::class);

// creates '$obj = new Info' and writes to $obj->processRefund and $obj->refundAmount

如果类有构造函数,则将结构体的元素作为命名参数传递给构造函数

class Info
{
	public function __construct(
		public bool $processRefund,
		public int $refundAmount,
	) {
	}
}

// creates $obj = new Info(processRefund: ..., refundAmount: ...)

与标量参数结合的类型转换会创建一个对象,并将值作为唯一参数传递给构造函数

Expect::string()->castTo(DateTime::class);
// creates new DateTime(...)

规范化:before()

在验证本身之前,可以使用 before() 方法对数据进行规范化。例如,有一个必须为数组字符串(例如 ['a', 'b', 'c'])的元素,但接收到的输入形式为字符串 a b c

$explode = fn($v) => explode(' ', $v);

$schema = Expect::arrayOf('string')
	->before($explode);

$normalized = $processor->process($schema, 'a b c');
// OK, returns ['a', 'b', 'c']

对象映射:from()

您可以从类生成结构模式。例如

class Config
{
	public string $name;
	public ?string $password;
	public bool $admin = false;
}

$schema = Expect::from(new Config);

$data = [
	'name' => 'jeff',
];

$normalized = $processor->process($schema, $data);
// $normalized instanceof Config
// $normalized = {'name' => 'jeff', 'password' => null, 'admin' => false}

也支持匿名类

$schema = Expect::from(new class {
	public string $name;
	public ?string $password;
	public bool $admin = false;
});

由于从类定义中获得的信息可能不足,您可以通过第二个参数为元素添加自定义模式

$schema = Expect::from(new Config, [
	'name' => Expect::string()->pattern('\w:.*'),
]);