Validating data structures against a given Schema.
A practical library for validating and normalizing data structures against a given schema with a smart, easy-to-understand API.
Documentation can be found on the website.
Installation:
composer require nette/schemaIt requires PHP version 8.1 and supports PHP up to 8.5.
Do you like Nette Schema? Are you looking forward to the new features? Thank you!
In the variable $schema, we have a validation schema (we'll explain what this means and how to create one in a moment), and in the variable $data, we have the data structure we want to validate and normalize. This could be, for example, data submitted by a user via an API, a configuration file, etc.
The task is handled by the Nette\Schema\Processor class, which processes the input and either returns normalized data or throws a Nette\Schema\ValidationException exception if an error occurs.
$processor = new Nette\Schema\Processor;
try {
$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
echo 'Data is invalid: ' . $e->getMessage();
}The method $e->getMessages() returns an array of all messages as strings, and $e->getMessageObjects() returns all messages as Nette\Schema\Message objects.
And now let's create the schema. The class Nette\Schema\Expect is used to define it; we essentially define expectations for what the data should look like. Let's say the input data must be a structure (e.g., an array) containing elements processRefund of type bool and refundAmount of type int.
use Nette\Schema\Expect;
$schema = Expect::structure([
'processRefund' => Expect::bool(),
'refundAmount' => Expect::int(),
]);We believe the schema definition looks understandable, even if you're seeing it for the first time.
Let's send the following data for validation:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, passes validationThe output, i.e., the value $normalized, is a stdClass object. If we wanted the output to be an array, we would add casting ->castTo('array') to the schema.
All elements of the structure are optional and have a default value of null. Example:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, passes validation
// $normalized = {'processRefund' => null, 'refundAmount' => 17}The fact that the default value is null does not mean it would accept 'processRefund' => null in the input data. No, the input must be a boolean, i.e. true or false only. We would have to explicitly allow null using Expect::bool()->nullable().
An item can be made mandatory using Expect::bool()->required(). We can change the default value, for example, to false using Expect::bool()->default(false) or shorthand Expect::bool(false).
And what if we wanted to accept 1 and 0 in addition to booleans? Then we list the values that we also want to normalize to 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); // trueNow you know the basics of defining a schema and how the structure items behave. We will now show what other elements you can use when defining a schema.
All standard PHP data types can be specified in the schema:
Expect::string($default = null)
Expect::int($default = null)
Expect::float($default = null)
Expect::bool($default = null)
Expect::null()
Expect::array($default = [])
Expect::list($default = [])And also all types supported by the Validators class, for example Expect::type('scalar') or shorthand Expect::scalar(). Also class or interface names, e.g., Expect::type('AddressEntity').
Union syntax can also be used:
Expect::type('bool|string|array')The default value is always null with the exception of array and list, where it is an empty array. (A list is an array indexed by a sequence of numeric keys starting from zero, i.e. a non-associative array).
An array represents too general a structure; it's more useful to specify precisely which elements it may contain. For example, an array whose elements can only be strings:
$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 stringThe second parameter can specify keys (since version 1.2):
$schema = Expect::arrayOf('string', 'int');
$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: 'a' is not an intA list is an indexed array:
$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: not a list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: also not a listWith the wrap parameter, a single value is also accepted and wrapped into a one-item list (since version 1.4):
$schema = Expect::listOf('string', wrap: true);
$processor->process($schema, 'hello'); // OK, gives ['hello']
$processor->process($schema, ['a', 'b']); // OKThe parameter can also be a schema, so we can write:
Expect::arrayOf(Expect::bool())The default value is an empty array. If you specify a default value and call mergeDefaults(), it will be merged with the passed data.
anyOf() represents a set of values or schemas that a value can take. Here's how to write an array of elements that can be either 'a', true, or 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 thereThe elements of the enumeration can also be schemas:
$schema = Expect::listOf(
Expect::anyOf(Expect::string(), true, null),
);
$processor->process($schema, ['foo', true, null, 'bar']); // OK
$processor->process($schema, [123]); // ERRORThe anyOf() method accepts variants as separate parameters, not as an array. To pass it an array of values, use the unpack operator anyOf(...$variants).
Variants are tried in the order they are written and the first matching one wins. The single exception is null: when it belongs to the accepted values, a null input always gives null, even though a structure or an array variant would otherwise read it as an empty value of its own.
The default value is null. Use the firstIsDefault() method to make the first item the default:
// default is 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();A configuration file can only carry a plain string like 'hearts', but in your code you would much rather work with an enum. That is the gap enum() closes (since version 1.4): give it an enum with a backing value (enum Suit: string) and it accepts either the backing value or the case itself, always handing you back the case:
enum Suit: string
{
case Hearts = 'hearts';
case Spades = 'spades';
}
$schema = Expect::enum(Suit::class);
$processor->process($schema, 'hearts'); // OK, returns Suit::Hearts
$processor->process($schema, Suit::Spades); // OK, returns Suit::Spades
$processor->process($schema, 'clubs'); // ERROR: expects to be SuitEnums backed by integers work exactly the same, and so does everything you know from the other elements, such as Expect::enum(Suit::class)->required() or ->default(Suit::Hearts).
You often do not have to reach for enum() explicitly. An enum class name is recognized in a type expression and when generating a schema from a class, so Expect::type(Suit::class) and a public Suit $suit property give you the very same element.
Structures are objects with defined keys. Each key-value pair is referred to as a "property".
Structures accept arrays and objects and return stdClass objects.
By default, all properties are optional and have a default value of null. You can define mandatory properties using required():
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
$processor->process($schema, ['optional' => '']);
// ERROR: option 'required' is missing
$processor->process($schema, ['required' => 'foo']);
// OK, returns {'required' => 'foo', 'optional' => null}A structure itself is mandatory. Therefore, if it is nested inside another structure and the input does not contain it, it is created anyway - and it reports an error when it contains a required property. Use required(false) to make the entire nested structure optional. If it is missing in the input, null appears in the output, but if it is present, its required properties are enforced:
$schema = Expect::structure([
'db' => Expect::structure([
'dsn' => Expect::string()->required(),
])->required(false),
]);
$processor->process($schema, []);
// OK, returns {'db' => null}
$processor->process($schema, ['db' => []]);
// ERROR: 'db › dsn' is missingIf you do not want properties with default values in the output, use skipDefaults():
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, returns {'required' => 'foo'}Although null is the default value for the optional property, it is not allowed in input data (the value must be a string). Properties accepting null are defined using 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}The array of all structure properties is returned by the getShape() method.
By default, no additional items can be present in the input data:
$schema = Expect::structure([
'key' => Expect::string(),
]);
$processor->process($schema, ['additional' => 1]);
// ERROR: Unexpected item 'additional'This can be changed using otherItems(). As a parameter, pass the schema to validate each extra item:
$schema = Expect::structure([
'key' => Expect::string(),
])->otherItems(Expect::int());
$processor->process($schema, ['additional' => 1]); // OK
$processor->process($schema, ['additional' => true]); // ERRORYou can create a new structure by extending another using extend():
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);An array with defined keys (since version 1.3.2). Ev
No open issues yet, or sync has not completed.