News in Nette Schema 1.2

8 months ago by David Grudl     edit

Nette Schema is the youngest addition to the Nette family. The library was originally created for the needs of Nette DI, ie to validate and normalize the input configuration files and to inform about possible errors.

Nette Schema was an attempt to find a comprehensible and economical language for describing data structures. The first version covered all the needs of Nette DI and further developments then addressed the requirements arising from new usages.

Let's recall how Schema is used. For example, we will create a schema, where the input should be an array (or an object with public properties, in the terminology Schema it is a structure), with a mandatory element foo of type bool and an optional numeric bar and string baz:

use Nette\Schema\Expect;

$schema = Expect::structure([
    'foo' => Expect::bool()->required(),
    'bar' => Expect::int()->default(123),
    'baz' => Expect::string(),
]);

We validate the input $data:

$processor = new Nette\Schema\Processor;
$normalized = $processor->process($schema, $data);
// in case of error it throws Nette\Schema\ValidationException

Inside structure(), unless otherwise stated, each element:

  • is optional
  • has a default value of null (resp. [] in the case of array)

Default value of null does not mean that null it also accepted – this would have to be stated in the definition, for example Expect::int()->nullable().

So for input $data = ['foo' => true] it returns object {foo: true, bar: 123, baz: null}, see demo. So we can access $normalized->bar without worrying that the property would be undefined.

Default structure values

If the element accepts null and at the same time has a default value of null, it is not possible to distinguish the default value from the passed value. If necessary, the default values ​​can be completely omitted since version 1.2 using skipDefaults():

$schema = Expect::structure([
	...
])->skipDefaults();

So for input $data = ['foo' => true] it returns object {foo: true}.

Mandatory / optional elements

As mentioned, only elements marked as required() are mandatory. But what if the structure is deeper? Is element bar mandatory?

$schema = Expect::structure([
	'foo' => Expect::int(),
	'inner' => Expect::structure([
		'bar' => Expect::string()->required(),
	]),
]);

In this case, the Schema tries to behave in such a way that it does not surprise the programmer, so that the element bar is mandatory, although strictly technically the element inner would also have to be marked as mandatory. The Schema makes it mandatory implicitly. See demo.

The structure can now be marked as truly optional using requred(false).

$schema = Expect::structure([
	'foo' => Expect::int(),
	'inner' => Expect::structure([
		'bar' => Expect::string()->required(),
	])->required(false),
]);

In this case, the data may not contain a key inner, but if it does, a subkey bar must be present.

Merging elements in arrays

One of the things that arose from the behavior of Nette DI, but proved surprising for users of a separate Schema, was the merging of default and passed values ​​in the case of arrays, ie Expect::array(), list(), etc. In Nette Schema 2, this behavior will be removed, it can now be turned off:

Expect::list([1, 2, 3])->mergeDefaults(false)

arrayOf() and keys

The arrayOf describes what elements an array can contain. For example, an array whose elements can only be strings is written as Expect::arrayOf('string').

It is now possible to specify the key type with the second parameter:

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

Default value of anyOf()

It is now easier to define the default value of enumeration anyOf() by declaring the first item as the default:

Expect::anyOf(
	Expect::string()->default('def'),
	false
)->firstIsDefault();

The default value of such an element is 'def'.

Richer error messages

If the data does not pass validation, the Nette\Schema\ValidationException exception is thrown. Its method getMessages() returns an array of all error messages.

New is the method getMessageObjects(), which returns array of errors as Nette\Schema\Message objects. They broke the report message into basic information:

$messages = $e->getMessageObjects();
$message = $messages[0];
$message->message   // message text with %placeholders% for variables
$message->code      // error code, one of the constants in the Message class
$message->path      // a arrays referring to the element to which the error relates
$message->variables // variables to replace %placeholders%

This makes it easy to translate messages into other languages.

Deprecated

And finally. Elements can be referred to as deprecated with an optional error message. The warning list then returns getWarnings().

use Nette\Schema\Expect;

$schema = Expect::structure([
	'foo' => Expect::bool()->deprecated('Use `bar` instead'),
	'bar' => Expect::bool(),
]);

$normalized = $processor->process($schema, $data);
$warnings = $processor->getWarnings(); // returns an array of strings