Schema: データ検証

与えられたスキーマに対してデータ構造を検証および正規化するための、スマートで理解しやすいAPIを備えた実用的なライブラリ。

インストール:

composer require nette/schema

基本的な使用法

変数$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 'データは有効ではありません: ' . $e->getMessage();
}

$e->getMessages()メソッドはすべてのメッセージを文字列の配列として返し、$e->getMessageObjects()はすべてのメッセージをNette\Schema\Message

スキーマの定義

そして今、スキーマを作成します。その定義にはNette\Schema\Expectクラスを使用し、データがどのように見えるべきかの期待を定義します。入力データは、bool型のprocessRefund要素とint型の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、検証を通過します

出力、つまり$normalizedの値はstdClassオブジェクトです。出力が配列になるようにしたい場合は、スキーマにExpect::structure([...])->castTo('array')という型キャストを追加します。

構造のすべての要素はオプションであり、デフォルト値はnullです。例:

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

$normalized = $processor->process($schema, $data); // OK、検証を通過します
// $normalized = {'processRefund' => null, 'refundAmount' => 17}

デフォルト値がnullであることは、入力データで'processRefund' => nullが受け入れられることを意味するわけではありません。いいえ、入力はブール値、つまりtrueまたはfalseのみでなければなりません。nullを許可するには、Expect::bool()->nullable()を使用して明示的に許可する必要があります。

項目はExpect::bool()->required()を使用して必須にすることができます。デフォルト値を例えばfalseに変更するには、Expect::bool()->default(false)または短縮してExpect::bool(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 = [])

さらに、Validatorsクラスでサポートされているすべての型、例えばExpect::type('scalar')または短縮してExpect::scalar()。クラス名やインターフェース名も、例えばExpect::type('AddressEntity')

union表記も使用できます:

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]); // エラー: 123 は文字列ではありません

2番目のパラメータでキーを指定できます(バージョン1.2以降):

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // エラー: 'a' は int ではありません

リストはインデックス付き配列です:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // エラー: 123 は文字列ではありません
$processor->process($schema, ['key' => 'a']); // エラー: リストではありません
$processor->process($schema, [1 => 'a', 0 => 'b']); // エラー: これもリストではありません

パラメータはスキーマにすることもできるので、次のように記述できます:

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

デフォルト値は空の配列です。デフォルト値を指定すると、渡されたデータとマージされます。これはmergeDefaults(false)で無効にできます(バージョン1.1以降)。

列挙: anyOf()

anyOf()は、値が取ることができる値またはスキーマのリストを表します。このようにして、要素が'a'true、またはnullのいずれかである配列を記述します:

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

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // エラー: false はそこに含まれません

列挙の要素はスキーマにすることもできます:

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

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

anyOf()メソッドは、バリアントを個別のパラメータとして受け入れ、配列としては受け入れません。値の配列を渡したい場合は、アンパック演算子anyOf(...$variants)を使用します。

デフォルト値はnullです。firstIsDefault()メソッドを使用すると、最初の要素がデフォルトになります:

// デフォルトは 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

構造

構造は、定義されたキーを持つオブジェクトです。各キー=>値のペアは「プロパティ」と呼ばれます:

構造は配列とオブジェクトを受け入れ、stdClassオブジェクトを返します。

デフォルトでは、すべてのプロパティはオプションであり、デフォルト値はnullです。必須プロパティはrequired()を使用して定義できます:

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // デフォルト値は null
]);

$processor->process($schema, ['optional' => '']);
// エラー: オプション 'required' がありません

$processor->process($schema, ['required' => 'foo']);
// OK、{'required' => 'foo', 'optional' => null} を返します

出力にデフォルト値を持つプロパティを含めたくない場合は、skipDefaults()を使用します:

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

$processor->process($schema, ['required' => 'foo']);
// OK、{'required' => 'foo'} を返します

nulloptionalプロパティのデフォルト値ですが、入力データでは許可されていません(値は文字列でなければなりません)。nullを受け入れるプロパティはnullable()を使用して定義します:

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

$processor->process($schema, ['optional' => null]);
// エラー: 'optional' は文字列を期待しますが、null が与えられました。

$processor->process($schema, ['nullable' => null]);
// OK、{'optional' => null, 'nullable' => null} を返します

構造のすべてのプロパティの配列はgetShape()メソッドで返されます。

デフォルトでは、入力データに余分な項目を含めることはできません:

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

$processor->process($schema, ['additional' => 1]);
// エラー: 予期しない項目 'additional'

これはotherItems()で変更できます。パラメータとして、余分な要素が検証されるスキーマを指定します:

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

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

extend()を使用して、別の構造から派生して新しい構造を作成できます:

$dog = Expect::structure([
	'name' => Expect::string(),
	'age' => Expect::int(),
]);

$dogWithBreed = $dog->extend([
	'breed' => Expect::string(),
]);

配列

定義されたキーを持つ配列。構造に適用されるすべてが適用されます。

$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // デフォルト値は null
]);

タプルとして知られるインデックス付き配列も定義できます:

$schema = Expect::array([
	Expect::int(),
	Expect::string(),
	Expect::bool(),
]);

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

非推奨のプロパティ

deprecated([string $message])メソッドを使用してプロパティを非推奨としてマークできます。サポート終了に関する情報は$processor->getWarnings()によって返されます:

$schema = Expect::structure([
	'old' => Expect::int()->deprecated('項目 %path% は非推奨です'),
]);

$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["項目 'old' は非推奨です"]

範囲: min() max()

min()max()を使用して、配列の要素数を制限できます:

// 配列、少なくとも10項目、最大20項目
Expect::array()->min(10)->max(20);

文字列の場合、その長さを制限します:

// 文字列、少なくとも10文字、最大20文字
Expect::string()->min(10)->max(20);

数値の場合、その値を制限します:

// 整数、10から20まで(両端を含む)
Expect::int()->min(10)->max(20);

もちろん、min()のみ、またはmax()のみを指定することも可能です:

// 文字列、最大20文字
Expect::string()->max(20);

正規表現: pattern()

pattern()を使用して、入力文字列全体が一致する必要がある正規表現を指定できます(つまり、^$文字で囲まれているかのように):

// ちょうど9桁の数字
Expect::string()->pattern('\d{9}');

カスタム制約: assert()

その他の制約はassert(callable $fn)を使用して指定します。

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

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // 個数は偶数でなければなりません

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // エラー: 3 は偶数ではありません

または

Expect::string()->assert('is_file'); // ファイルが存在する必要があります

各制約にカスタムの説明を追加できます。これはエラーメッセージの一部になります。

$schema = Expect::arrayOf('string')
	->assert($countIsEven, '配列内の偶数項目');

$processor->process($schema, ['a', 'b', 'c']);
// 値配列を持つ項目のアサーション "配列内の偶数項目" に失敗しました。

このメソッドは繰り返し呼び出して、複数の制約を追加できます。transform()およびcastTo()の呼び出しと交互に使用できます。

変換: transform()

正常に検証されたデータは、カスタム関数を使用して変更できます:

// 大文字への変換:
Expect::string()->transform(fn(string $s) => strtoupper($s));

このメソッドは繰り返し呼び出して、複数の変換を追加できます。assert()およびcastTo()の呼び出しと交互に使用できます。操作は宣言された順序で実行されます:

Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'すべての文字は小文字でなければなりません')
	->transform(fn(string $s) => strtoupper($s)); // 大文字への変換

transform()メソッドは、同時に値を変換および検証できます。これは、transform()assert()を連鎖させるよりも、多くの場合、より簡単で重複が少なくなります。この目的のために、関数はContextオブジェクトを受け取り、addError()メソッドを使用して検証の問題に関する情報を追加できます:

Expect::string()
	->transform(function (string $s, Nette\Schema\Context $context) {
		if (!ctype_lower($s)) {
			$context->addError('すべての文字は小文字でなければなりません', '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);

// '$obj = new Info' を作成し、$obj->processRefund と $obj->refundAmount に書き込みます

クラスにコンストラクタがある場合、構造の要素は名前付きパラメータとしてコンストラクタに渡されます:

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

// $obj = new Info(processRefund: ..., refundAmount: ...) を作成します

スカラーパラメータと組み合わせた型キャストは、オブジェクトを作成し、値を単一のパラメータとしてコンストラクタに渡します:

Expect::string()->castTo(DateTime::class);
// 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 で ['a', 'b', 'c'] を返します

オブジェクトへのマッピング: from()

構造スキーマをクラスから生成させることができます。例:

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

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

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

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

匿名クラスもサポートされています:

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

クラス定義から取得した情報だけでは不十分な場合があるため、2番目のパラメータを使用して要素にカスタムスキーマを追加できます:

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