スキーマデータバリデーション

与えられたスキーマに対してデータ構造を検証し、正規化するための実用的なライブラリで、スマートで理解しやすい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 'Data is invalid: ' . $e->getMessage();
}

Method$e->getMessages() は全てのメッセージ文字列の配列を返し、$e->getMessageObjects() は全てのメッセージをNetteSchema FilterMessage

スキーマの定義

そして、今度はスキーマを作成してみましょう。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, 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 で受け入れられることを意味しない。いいえ、入力はブーリアンでなければなりません。つまり、true またはfalse だけです。nullExpect::bool()->nullable() 経由で明示的に許可しなければならないでしょう。

項目はExpect::bool()->required() を使って必須とすることができる。Expect::bool()->default(false) を使ってデフォルト値をfalse に変更したり、Expect::bool(false) を使ってデフォルト値を間もなく変更したりします。

そして、ブーリアン以外に1 and 0 も受け入れたい場合はどうすればよいでしょうか。そこで、許容される値をリストアップし、これもbooleanに正規化することにします。

$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') のようになります。

また、ユニオン表記も使用できます。

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

デフォルト値は,arraylist を除いて,常にnull で,これは空の配列である.(リストは,ゼロから数値キーの昇順でインデックスが付けられた配列,つまり非結合型配列です).

値の配列: 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

2 番目のパラメータで、キーを指定することができます (バージョン 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())

デフォルト値は空の配列です。default value を指定すると、渡されたデータにマージされます。これを無効にするには、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]); // 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

デフォルトでは、すべてのプロパティはオプションであり、デフォルト値は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'}

nulloptional プロパティのデフォルト値であるが、入力データでは許されない(値は文字列でなければならない)。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}

すべての構造体プロパティの配列は、getShape() メソッドによって返される。

デフォルトでは、入力データに余分な項目は存在できない。

$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

extend() を使って別の構造から派生させることで、新しい構造を作成できます:

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

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

配列.toc-array

定義されたキーを持つ配列。構造体と同じルールが適用されます。

$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // default value is 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('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() を使うと、入力文字列の 全体 にマッチしなければならない正規表現を指定できます (つまり、入力文字列が^ a $ という文字でくくられたようなものです)。

// 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() メソッドを持つ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|null $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;
});

クラス定義から得られる情報だけでは十分でない可能性があるため、2番目のパラメータで要素のカスタム・スキーマを追加できます:

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