Nette Documentation Preview

syntax
Nette Schema
************

.[perex]
Una libreria pratica per validare e normalizzare le strutture dati rispetto a uno schema dato, con un'API intelligente e facile da capire.

Installazione:

```shell
composer require nette/schema
```


Uso di base
-----------

Nella variabile `$schema` abbiamo uno schema di validazione (che cosa significhi e come crearlo lo spieghiamo tra un attimo) e nella variabile `$data` la struttura dati che vogliamo validare e normalizzare. Possono essere per esempio dati inviati da un utente tramite un'API, un file di configurazione ecc.

Del compito si occupa la classe [api:Nette\Schema\Processor], che elabora l'input e restituisce i dati normalizzati, oppure lancia un'eccezione [api:Nette\Schema\ValidationException] in caso di errore.

```php
$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'I dati non sono validi: ' . $e->getMessage();
}
```

Il metodo `$e->getMessages()` restituisce un array di tutti i messaggi come stringhe e `$e->getMessageObjects()` restituisce tutti i messaggi come oggetti "Nette\Schema\Message":https://api.nette.org/schema/master/Nette/Schema/Message.html.


Definire lo schema
------------------

E ora creiamo lo schema. Per definirlo serve la classe [api:Nette\Schema\Expect]; in sostanza definiamo le aspettative su come i dati dovrebbero apparire. Diciamo che i dati in ingresso devono essere una struttura (per esempio un array) che contiene gli elementi `processRefund` di tipo bool e `refundAmount` di tipo int.

```php
use Nette\Schema\Expect;

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

Crediamo che la definizione dello schema appaia comprensibile, anche se la vedete per la prima volta.

Inviamo alla validazione questi dati:

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

$normalized = $processor->process($schema, $data); // OK, supera la validazione
```

L'output, cioè il valore `$normalized`, è un oggetto `stdClass`. Se volessimo che l'output fosse un array, aggiungeremmo allo schema la conversione `->castTo('array')`.

Tutti gli elementi della struttura sono facoltativi e hanno il valore predefinito `null`. Esempio:

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

$normalized = $processor->process($schema, $data); // OK, supera la validazione
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
```

Il fatto che il valore predefinito sia `null` non significa che nei dati in ingresso verrebbe accettato `'processRefund' => null`. No, l'input deve essere un booleano, cioè solo `true` oppure `false`. Dovremmo consentire esplicitamente `null` con `Expect::bool()->nullable()`.

Un elemento si può rendere obbligatorio con `Expect::bool()->required()`. Il valore predefinito lo possiamo cambiare per esempio in `false` con `Expect::bool()->default(false)` oppure con la forma abbreviata `Expect::bool(false)`.

E se volessimo accettare, oltre ai booleani, anche `1` e `0`? Allora elenchiamo i valori che vogliamo normalizzare in booleano:

```php
$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
```

Ora conoscete le basi di come si definisce uno schema e di come si comportano gli elementi della struttura. Mostreremo ora quali altri elementi potete usare quando definite uno schema.


Tipi di dato: type()
--------------------

Nello schema si possono indicare tutti i tipi di dato standard di PHP:

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

E anche tutti i tipi [supportati dalla classe Validators |utils:validators#Tipi attesi], per esempio `Expect::type('scalar')` oppure la forma abbreviata `Expect::scalar()`. E i nomi di classi o interfacce, per esempio `Expect::type('AddressEntity')`.

Si può usare anche la sintassi delle unioni:

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

Il valore predefinito è sempre `null`, tranne per `array` e `list`, dove è un array vuoto. (Una list è un array indicizzato da una sequenza di chiavi numeriche a partire da zero, cioè un array non associativo.)


Array di valori: arrayOf() listOf()
-----------------------------------

Un array rappresenta una struttura troppo generica; è più utile indicare con precisione quali elementi può contenere. Per esempio un array i cui elementi possono essere solo stringhe:

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERRORE: 123 non è una stringa
```

Il secondo parametro può indicare le chiavi (dalla versione 1.2):

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERRORE: 'a' non è un int
```

Una list è un array indicizzato:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERRORE: 123 non è una stringa
$processor->process($schema, ['key' => 'a']); // ERRORE: non è una list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERRORE: anche questa non è una list
```

Il parametro può essere anche uno schema, quindi possiamo scrivere:

```php
Expect::arrayOf(Expect::bool())
```

Il valore predefinito è un array vuoto. Se indicate un valore predefinito, verrà fuso con i dati passati. Lo si può disattivare con `mergeDefaults(false)` (dalla versione 1.1).


Enumerazione: anyOf()
---------------------

`anyOf()` rappresenta un insieme di valori o di schemi che un valore può assumere. Ecco come scrivere un array di elementi che possono essere `'a'`, `true` oppure `null`:

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

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ERRORE: false non ci sta
```

Anche gli elementi dell'enumerazione possono essere schemi:

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

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

Il metodo `anyOf()` accetta le varianti come parametri separati, non come array. Per passargli un array di valori usate l'operatore di unpacking `anyOf(...$variants)`.

Il valore predefinito è `null`. Usate il metodo `firstIsDefault()` per rendere predefinito il primo elemento:

```php
// il predefinito è 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
```


Strutture
---------

Le strutture sono oggetti con chiavi definite. Ogni coppia chiave-valore si chiama "proprietà".

Le strutture accettano array e oggetti e restituiscono oggetti `stdClass`.

Per impostazione predefinita tutte le proprietà sono facoltative e hanno il valore predefinito `null`. Potete definire proprietà obbligatorie con `required()`:

```php
$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // il valore predefinito è null
]);

$processor->process($schema, ['optional' => '']);
// ERRORE: manca l'opzione 'required'

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

Una struttura è di per sé obbligatoria. Se quindi è annidata dentro un'altra struttura e l'input non la contiene, viene comunque creata, e segnala un errore quando contiene una proprietà obbligatoria. Usate `required(false)` per rendere facoltativa l'intera struttura annidata. Se manca nell'input, nell'output compare `null`, ma se è presente, le sue proprietà obbligatorie vengono applicate:

```php
$schema = Expect::structure([
	'db' => Expect::structure([
		'dsn' => Expect::string()->required(),
	])->required(false),
]);

$processor->process($schema, []);
// OK, restituisce {'db' => null}

$processor->process($schema, ['db' => []]);
// ERRORE: manca 'db › dsn'
```

Se non volete nell'output le proprietà con il valore predefinito, usate `skipDefaults()`:

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

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

Anche se `null` è il valore predefinito della proprietà `optional`, nei dati in ingresso non è consentito (il valore deve essere una stringa). Le proprietà che accettano `null` si definiscono con `nullable()`:

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

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

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

L'array di tutte le proprietà della struttura lo restituisce il metodo `getShape()`.

Per impostazione predefinita nei dati in ingresso non possono esserci elementi aggiuntivi:

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

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

Lo si può cambiare con `otherItems()`. Come parametro passate lo schema con cui validare ogni elemento in più:

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

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

Potete creare una nuova struttura estendendone un'altra con `extend()`:

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

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


Array .{data-version:1.3.2}
---------------------------

Un array con chiavi definite. Per esso vale tutto ciò che vale per le [strutture |#Strutture].

```php
$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // il valore predefinito è null
]);
```

Potete definire anche un array indicizzato, la cosiddetta tupla:

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

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


Proprietà deprecate
-------------------

Potete contrassegnare una proprietà come deprecata con il metodo `deprecated([string $message])`. L'informazione sulla deprecazione la restituisce `$processor->getWarnings()`:

```php
$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"]
```


Intervalli: min() max()
-----------------------

Usate `min()` e `max()` per limitare il numero di elementi degli array:

```php
// array, almeno 10 elementi, al massimo 20
Expect::array()->min(10)->max(20);
```

Per le stringhe limitate la lunghezza:

```php
// stringa, lunga almeno 10 caratteri, al massimo 20
Expect::string()->min(10)->max(20);
```

Per i numeri limitate il valore:

```php
// intero, tra 10 e 20 compresi
Expect::int()->min(10)->max(20);
```

Naturalmente si può indicare solo `min()` oppure solo `max()`:

```php
// stringa, al massimo 20 caratteri
Expect::string()->max(20);
```


Espressioni regolari: pattern()
-------------------------------

Con `pattern()` potete indicare un'espressione regolare a cui l'**intera** stringa in ingresso deve corrispondere (cioè come se fosse racchiusa tra i caratteri `^` e `$`):

```php
// esattamente 9 cifre
Expect::string()->pattern('\d{9}');
```


Asserzioni personalizzate: assert()
-----------------------------------

Potete aggiungere qualsiasi altro vincolo con `assert(callable $fn)`.

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

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // il numero deve essere pari

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERRORE: 3 non è un numero pari
```

Oppure

```php
Expect::string()->assert('is_file'); // il file deve esistere
```

A ogni asserzione potete aggiungere una descrizione personalizzata. Farà parte del messaggio di errore.

```php
$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.
```

Il metodo si può chiamare ripetutamente per aggiungere più vincoli. Si può alternare alle chiamate di `transform()` e `castTo()`.


Trasformazione: transform() .{data-version:1.2.5}
-------------------------------------------------

I dati validati con successo si possono modificare con una funzione personalizzata:

```php
// conversione in maiuscolo:
Expect::string()->transform(fn(string $s) => strtoupper($s));
```

Il metodo si può chiamare ripetutamente per aggiungere più trasformazioni. Si può alternare alle chiamate di `assert()` e `castTo()`. Le operazioni vengono eseguite nell'ordine in cui sono dichiarate:

```php
Expect::type('string|int')
	->castTo('string')
	->assert('ctype_lower', 'All characters must be lowercased')
	->transform(fn(string $s) => strtoupper($s)); // conversione in maiuscolo
```

Il metodo `transform()` può trasformare e validare il valore allo stesso tempo. Spesso è più semplice e con meno duplicazione di codice che concatenare `transform()` e `assert()`. A questo scopo la funzione riceve un oggetto [Context |api:Nette\Schema\Context] con il metodo `addError()`, che si può usare per aggiungere informazioni sui problemi di validazione:

```php
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);
	});
```


Conversione: castTo()
---------------------

I dati validati con successo si possono convertire:

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

Oltre ai tipi nativi di PHP potete convertire anche in classi. Si distingue tra una classe semplice senza costruttore e una classe con costruttore. Se la classe non ha un costruttore, viene creata un'istanza e tutti gli elementi della struttura vengono scritti nelle proprietà:

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

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

// crea '$obj = new Info' e scrive in $obj->processRefund e $obj->refundAmount
```

Se la classe ha un costruttore, gli elementi della struttura vengono passati al costruttore come argomenti con nome:

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

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

La conversione combinata con un parametro scalare crea un oggetto e passa il valore come unico argomento al costruttore:

```php
Expect::string()->castTo(DateTime::class);
// crea new DateTime(...)
```


Normalizzazione: before()
-------------------------

Prima della validazione vera e propria i dati si possono normalizzare con il metodo `before()`. Come esempio prendiamo un elemento che deve essere un array di stringhe (per esempio `['a', 'b', 'c']`), ma accetta l'input nella forma della stringa `a b c`:

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

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

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


Mappatura sugli oggetti: from()
-------------------------------

Potete far generare lo schema della struttura da una classe. Esempio:

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

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

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

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

Sono supportate anche le classi anonime:

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

Poiché le informazioni ricavate dalla definizione della classe possono non bastare, potete integrare gli elementi con un vostro schema usando il secondo parametro:

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


Unire più configurazioni
------------------------

Le applicazioni compongono spesso la propria configurazione a strati: ci sono i valori predefiniti integrati e sopra di essi l'utente fornisce le proprie impostazioni, che dovrebbero sovrascrivere solo gli elementi che indica davvero. È esattamente ciò che fa `processMultiple()`: prende più insiemi di dati, li unisce nell'ordine indicato in modo che i successivi abbiano la precedenza, e valida il risultato finale nel suo insieme:

```php
$schema = Expect::structure([
	'host' => Expect::string(),
	'port' => Expect::int(),
	'logging' => Expect::bool(),
]);

$defaults = ['host' => 'localhost', 'port' => 3306, 'logging' => false];
$userConfig = ['port' => 5432, 'logging' => true];

$config = $processor->processMultiple($schema, [$defaults, $userConfig]);
// $config = {'host' => 'localhost', 'port' => 5432, 'logging' => true}
```

L'elemento `host` mantiene il valore predefinito perché l'utente non lo ha impostato, mentre `port` e `logging` vengono sovrascritti dall'insieme di dati successivo. I valori salvati sotto chiavi stringa si uniscono in questo modo; gli elementi indicizzati numericamente (le list) vengono accodati uno dopo l'altro invece di essere sovrascritti.


Sotto il cofano: normalize, merge, complete
-------------------------------------------

Ogni elemento di schema, sia integrato sia scritto da voi, implementa quattro metodi che insieme definiscono come tratta i dati. Tre di essi formano la pipeline di elaborazione:

1. **normalize()** - prepara l'input grezzo. Qui girano gli hook di `before()` e qui, per esempio, un oggetto viene trasformato in array. Viene eseguito per primo, separatamente su ogni insieme di dati.
2. **merge()** - combina due insiemi di dati già normalizzati, dando la priorità al successivo. Questo passo lo usa solo `processMultiple()`; `process()` lo salta, perché ha un solo insieme di dati.
3. **complete()** - esegue la validazione vera e propria, riempie i valori predefiniti degli elementi mancanti e applica `assert()`, `transform()` e `castTo()`. Viene eseguito per ultimo, sul risultato unito.

Il quarto metodo, **completeDefault()**, viene chiamato dall'elemento genitore per un elemento del tutto assente dall'input: o fornisce il valore predefinito, o segnala che manca un elemento `required()`.

`process()` esegue quindi *normalize → complete*, mentre `processMultiple()` esegue *normalize (per ogni insieme di dati) → merge → complete*. È per questo ordine che `before()` vede l'input grezzo, mentre `transform()` vede il valore già validato.


Elementi di schema personalizzati
---------------------------------

Con `assert()`, `transform()` e `before()` si arriva lontano, quindi raramente serve costruire qualcosa da zero. Ma quando volete un elemento riutilizzabile e autonomo con la propria logica di validazione e di unione, potete crearlo implementando l'interfaccia [api:Nette\Schema\Schema]. Ha esattamente i quattro metodi descritti sopra:

```php
interface Schema
{
	function normalize(mixed $value, Context $context);
	function merge(mixed $value, mixed $base);
	function complete(mixed $value, Context $context);
	function completeDefault(Context $context);
}
```

Gli errori non si lanciano; li segnalate invece tramite l'oggetto [Context |api:Nette\Schema\Context] con `$context->addError()` e restituite `null`. Il `Processor` raccoglie tutti gli errori e li lancia insieme alla fine.

Come esempio costruiamo un elemento riutilizzabile che accetta il valore di supporto di un enum (per esempio la stringa `'hearts'`) e restituisce l'istanza dell'enum:

```php
use Nette\Schema\Context;
use Nette\Schema\Schema;

class EnumSchema implements Schema
{
	public function __construct(
		private string $enumClass,
	) {
	}

	public function normalize(mixed $value, Context $context): mixed
	{
		return $value; // non serve alcuna pre-elaborazione
	}

	public function merge(mixed $value, mixed $base): mixed
	{
		return $value ?? $base; // vince il valore successivo
	}

	public function complete(mixed $value, Context $context): mixed
	{
		$enum = is_string($value) ? ($this->enumClass)::tryFrom($value) : null;
		if ($enum === null) {
			$context->addError('The item %path% is not a valid value.', 'enum.value');
			return null;
		}

		return $enum;
	}

	public function completeDefault(Context $context): mixed
	{
		return null; // valore usato quando l'elemento manca dall'input
	}
}
```

Lo potete usare ovunque ci si aspetti un elemento integrato: da solo oppure come parte di una struttura più grande:

```php
enum Suit: string
{
	case Hearts = 'hearts';
	case Spades = 'spades';
}

$schema = Expect::structure([
	'suit' => new EnumSchema(Suit::class),
]);

$processor->process($schema, ['suit' => 'hearts']);
// OK, restituisce {'suit' => Suit::Hearts}
```

Poiché l'elemento implementa tutta l'interfaccia, funziona automaticamente anche dentro `processMultiple()`: il `Processor` chiama il suo metodo `merge()` proprio come per qualsiasi altro elemento.

Nette Schema

Una libreria pratica per validare e normalizzare le strutture dati rispetto a uno schema dato, con un'API intelligente e facile da capire.

Installazione:

composer require nette/schema

Uso di base

Nella variabile $schema abbiamo uno schema di validazione (che cosa significhi e come crearlo lo spieghiamo tra un attimo) e nella variabile $data la struttura dati che vogliamo validare e normalizzare. Possono essere per esempio dati inviati da un utente tramite un'API, un file di configurazione ecc.

Del compito si occupa la classe Nette\Schema\Processor, che elabora l'input e restituisce i dati normalizzati, oppure lancia un'eccezione Nette\Schema\ValidationException in caso di errore.

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'I dati non sono validi: ' . $e->getMessage();
}

Il metodo $e->getMessages() restituisce un array di tutti i messaggi come stringhe e $e->getMessageObjects() restituisce tutti i messaggi come oggetti Nette\Schema\Message.

Definire lo schema

E ora creiamo lo schema. Per definirlo serve la classe Nette\Schema\Expect; in sostanza definiamo le aspettative su come i dati dovrebbero apparire. Diciamo che i dati in ingresso devono essere una struttura (per esempio un array) che contiene gli elementi processRefund di tipo bool e refundAmount di tipo int.

use Nette\Schema\Expect;

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

Crediamo che la definizione dello schema appaia comprensibile, anche se la vedete per la prima volta.

Inviamo alla validazione questi dati:

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

$normalized = $processor->process($schema, $data); // OK, supera la validazione

L'output, cioè il valore $normalized, è un oggetto stdClass. Se volessimo che l'output fosse un array, aggiungeremmo allo schema la conversione ->castTo('array').

Tutti gli elementi della struttura sono facoltativi e hanno il valore predefinito null. Esempio:

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

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

Il fatto che il valore predefinito sia null non significa che nei dati in ingresso verrebbe accettato 'processRefund' => null. No, l'input deve essere un booleano, cioè solo true oppure false. Dovremmo consentire esplicitamente null con Expect::bool()->nullable().

Un elemento si può rendere obbligatorio con Expect::bool()->required(). Il valore predefinito lo possiamo cambiare per esempio in false con Expect::bool()->default(false) oppure con la forma abbreviata Expect::bool(false).

E se volessimo accettare, oltre ai booleani, anche 1 e 0? Allora elenchiamo i valori che vogliamo normalizzare in booleano:

$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

Ora conoscete le basi di come si definisce uno schema e di come si comportano gli elementi della struttura. Mostreremo ora quali altri elementi potete usare quando definite uno schema.

Tipi di dato: type()

Nello schema si possono indicare tutti i tipi di dato standard di PHP:

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

E anche tutti i tipi supportati dalla classe Validators, per esempio Expect::type('scalar') oppure la forma abbreviata Expect::scalar(). E i nomi di classi o interfacce, per esempio Expect::type('AddressEntity').

Si può usare anche la sintassi delle unioni:

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

Il valore predefinito è sempre null, tranne per array e list, dove è un array vuoto. (Una list è un array indicizzato da una sequenza di chiavi numeriche a partire da zero, cioè un array non associativo.)

Array di valori: arrayOf() listOf()

Un array rappresenta una struttura troppo generica; è più utile indicare con precisione quali elementi può contenere. Per esempio un array i cui elementi possono essere solo stringhe:

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello', 'b' => 'world']); // OK
$processor->process($schema, ['key' => 123]); // ERRORE: 123 non è una stringa

Il secondo parametro può indicare le chiavi (dalla versione 1.2):

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERRORE: 'a' non è un int

Una list è un array indicizzato:

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

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERRORE: 123 non è una stringa
$processor->process($schema, ['key' => 'a']); // ERRORE: non è una list
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERRORE: anche questa non è una list

Il parametro può essere anche uno schema, quindi possiamo scrivere:

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

Il valore predefinito è un array vuoto. Se indicate un valore predefinito, verrà fuso con i dati passati. Lo si può disattivare con mergeDefaults(false) (dalla versione 1.1).

Enumerazione: anyOf()

anyOf() rappresenta un insieme di valori o di schemi che un valore può assumere. Ecco come scrivere un array di elementi che possono essere 'a', true oppure null:

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

$processor->process($schema, ['a', true, null, 'a']); // OK
$processor->process($schema, ['a', false]); // ERRORE: false non ci sta

Anche gli elementi dell'enumerazione possono essere schemi:

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

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

Il metodo anyOf() accetta le varianti come parametri separati, non come array. Per passargli un array di valori usate l'operatore di unpacking anyOf(...$variants).

Il valore predefinito è null. Usate il metodo firstIsDefault() per rendere predefinito il primo elemento:

// il predefinito è 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

Strutture

Le strutture sono oggetti con chiavi definite. Ogni coppia chiave-valore si chiama „proprietà“.

Le strutture accettano array e oggetti e restituiscono oggetti stdClass.

Per impostazione predefinita tutte le proprietà sono facoltative e hanno il valore predefinito null. Potete definire proprietà obbligatorie con required():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // il valore predefinito è null
]);

$processor->process($schema, ['optional' => '']);
// ERRORE: manca l'opzione 'required'

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

Una struttura è di per sé obbligatoria. Se quindi è annidata dentro un'altra struttura e l'input non la contiene, viene comunque creata, e segnala un errore quando contiene una proprietà obbligatoria. Usate required(false) per rendere facoltativa l'intera struttura annidata. Se manca nell'input, nell'output compare null, ma se è presente, le sue proprietà obbligatorie vengono applicate:

$schema = Expect::structure([
	'db' => Expect::structure([
		'dsn' => Expect::string()->required(),
	])->required(false),
]);

$processor->process($schema, []);
// OK, restituisce {'db' => null}

$processor->process($schema, ['db' => []]);
// ERRORE: manca 'db › dsn'

Se non volete nell'output le proprietà con il valore predefinito, usate skipDefaults():

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

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

Anche se null è il valore predefinito della proprietà optional, nei dati in ingresso non è consentito (il valore deve essere una stringa). Le proprietà che accettano null si definiscono con nullable():

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

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

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

L'array di tutte le proprietà della struttura lo restituisce il metodo getShape().

Per impostazione predefinita nei dati in ingresso non possono esserci elementi aggiuntivi:

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

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

Lo si può cambiare con otherItems(). Come parametro passate lo schema con cui validare ogni elemento in più:

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

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

Potete creare una nuova struttura estendendone un'altra con extend():

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

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

Array

Un array con chiavi definite. Per esso vale tutto ciò che vale per le strutture.

$schema = Expect::array([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // il valore predefinito è null
]);

Potete definire anche un array indicizzato, la cosiddetta tupla:

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

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

Proprietà deprecate

Potete contrassegnare una proprietà come deprecata con il metodo deprecated([string $message]). L'informazione sulla deprecazione la restituisce $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"]

Intervalli: min() max()

Usate min() e max() per limitare il numero di elementi degli array:

// array, almeno 10 elementi, al massimo 20
Expect::array()->min(10)->max(20);

Per le stringhe limitate la lunghezza:

// stringa, lunga almeno 10 caratteri, al massimo 20
Expect::string()->min(10)->max(20);

Per i numeri limitate il valore:

// intero, tra 10 e 20 compresi
Expect::int()->min(10)->max(20);

Naturalmente si può indicare solo min() oppure solo max():

// stringa, al massimo 20 caratteri
Expect::string()->max(20);

Espressioni regolari: pattern()

Con pattern() potete indicare un'espressione regolare a cui l'intera stringa in ingresso deve corrispondere (cioè come se fosse racchiusa tra i caratteri ^ e $):

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

Asserzioni personalizzate: assert()

Potete aggiungere qualsiasi altro vincolo con assert(callable $fn).

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

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // il numero deve essere pari

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERRORE: 3 non è un numero pari

Oppure

Expect::string()->assert('is_file'); // il file deve esistere

A ogni asserzione potete aggiungere una descrizione personalizzata. Farà parte del messaggio di errore.

$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.

Il metodo si può chiamare ripetutamente per aggiungere più vincoli. Si può alternare alle chiamate di transform() e castTo().

Trasformazione: transform()

I dati validati con successo si possono modificare con una funzione personalizzata:

// conversione in maiuscolo:
Expect::string()->transform(fn(string $s) => strtoupper($s));

Il metodo si può chiamare ripetutamente per aggiungere più trasformazioni. Si può alternare alle chiamate di assert() e castTo(). Le operazioni vengono eseguite nell'ordine in cui sono dichiarate:

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

Il metodo transform() può trasformare e validare il valore allo stesso tempo. Spesso è più semplice e con meno duplicazione di codice che concatenare transform() e assert(). A questo scopo la funzione riceve un oggetto Context con il metodo addError(), che si può usare per aggiungere informazioni sui problemi di validazione:

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);
	});

Conversione: castTo()

I dati validati con successo si possono convertire:

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

Oltre ai tipi nativi di PHP potete convertire anche in classi. Si distingue tra una classe semplice senza costruttore e una classe con costruttore. Se la classe non ha un costruttore, viene creata un'istanza e tutti gli elementi della struttura vengono scritti nelle proprietà:

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

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

// crea '$obj = new Info' e scrive in $obj->processRefund e $obj->refundAmount

Se la classe ha un costruttore, gli elementi della struttura vengono passati al costruttore come argomenti con nome:

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

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

La conversione combinata con un parametro scalare crea un oggetto e passa il valore come unico argomento al costruttore:

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

Normalizzazione: before()

Prima della validazione vera e propria i dati si possono normalizzare con il metodo before(). Come esempio prendiamo un elemento che deve essere un array di stringhe (per esempio ['a', 'b', 'c']), ma accetta l'input nella forma della stringa a b c:

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

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

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

Mappatura sugli oggetti: from()

Potete far generare lo schema della struttura da una classe. Esempio:

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

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

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

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

Sono supportate anche le classi anonime:

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

Poiché le informazioni ricavate dalla definizione della classe possono non bastare, potete integrare gli elementi con un vostro schema usando il secondo parametro:

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

Unire più configurazioni

Le applicazioni compongono spesso la propria configurazione a strati: ci sono i valori predefiniti integrati e sopra di essi l'utente fornisce le proprie impostazioni, che dovrebbero sovrascrivere solo gli elementi che indica davvero. È esattamente ciò che fa processMultiple(): prende più insiemi di dati, li unisce nell'ordine indicato in modo che i successivi abbiano la precedenza, e valida il risultato finale nel suo insieme:

$schema = Expect::structure([
	'host' => Expect::string(),
	'port' => Expect::int(),
	'logging' => Expect::bool(),
]);

$defaults = ['host' => 'localhost', 'port' => 3306, 'logging' => false];
$userConfig = ['port' => 5432, 'logging' => true];

$config = $processor->processMultiple($schema, [$defaults, $userConfig]);
// $config = {'host' => 'localhost', 'port' => 5432, 'logging' => true}

L'elemento host mantiene il valore predefinito perché l'utente non lo ha impostato, mentre port e logging vengono sovrascritti dall'insieme di dati successivo. I valori salvati sotto chiavi stringa si uniscono in questo modo; gli elementi indicizzati numericamente (le list) vengono accodati uno dopo l'altro invece di essere sovrascritti.

Sotto il cofano: normalize, merge, complete

Ogni elemento di schema, sia integrato sia scritto da voi, implementa quattro metodi che insieme definiscono come tratta i dati. Tre di essi formano la pipeline di elaborazione:

  1. normalize() – prepara l'input grezzo. Qui girano gli hook di before() e qui, per esempio, un oggetto viene trasformato in array. Viene eseguito per primo, separatamente su ogni insieme di dati.
  2. merge() – combina due insiemi di dati già normalizzati, dando la priorità al successivo. Questo passo lo usa solo processMultiple(); process() lo salta, perché ha un solo insieme di dati.
  3. complete() – esegue la validazione vera e propria, riempie i valori predefiniti degli elementi mancanti e applica assert(), transform() e castTo(). Viene eseguito per ultimo, sul risultato unito.

Il quarto metodo, completeDefault(), viene chiamato dall'elemento genitore per un elemento del tutto assente dall'input: o fornisce il valore predefinito, o segnala che manca un elemento required().

process() esegue quindi normalize → complete, mentre processMultiple() esegue normalize (per ogni insieme di dati) → merge → complete. È per questo ordine che before() vede l'input grezzo, mentre transform() vede il valore già validato.

Elementi di schema personalizzati

Con assert(), transform() e before() si arriva lontano, quindi raramente serve costruire qualcosa da zero. Ma quando volete un elemento riutilizzabile e autonomo con la propria logica di validazione e di unione, potete crearlo implementando l'interfaccia Nette\Schema\Schema. Ha esattamente i quattro metodi descritti sopra:

interface Schema
{
	function normalize(mixed $value, Context $context);
	function merge(mixed $value, mixed $base);
	function complete(mixed $value, Context $context);
	function completeDefault(Context $context);
}

Gli errori non si lanciano; li segnalate invece tramite l'oggetto Context con $context->addError() e restituite null. Il Processor raccoglie tutti gli errori e li lancia insieme alla fine.

Come esempio costruiamo un elemento riutilizzabile che accetta il valore di supporto di un enum (per esempio la stringa 'hearts') e restituisce l'istanza dell'enum:

use Nette\Schema\Context;
use Nette\Schema\Schema;

class EnumSchema implements Schema
{
	public function __construct(
		private string $enumClass,
	) {
	}

	public function normalize(mixed $value, Context $context): mixed
	{
		return $value; // non serve alcuna pre-elaborazione
	}

	public function merge(mixed $value, mixed $base): mixed
	{
		return $value ?? $base; // vince il valore successivo
	}

	public function complete(mixed $value, Context $context): mixed
	{
		$enum = is_string($value) ? ($this->enumClass)::tryFrom($value) : null;
		if ($enum === null) {
			$context->addError('The item %path% is not a valid value.', 'enum.value');
			return null;
		}

		return $enum;
	}

	public function completeDefault(Context $context): mixed
	{
		return null; // valore usato quando l'elemento manca dall'input
	}
}

Lo potete usare ovunque ci si aspetti un elemento integrato: da solo oppure come parte di una struttura più grande:

enum Suit: string
{
	case Hearts = 'hearts';
	case Spades = 'spades';
}

$schema = Expect::structure([
	'suit' => new EnumSchema(Suit::class),
]);

$processor->process($schema, ['suit' => 'hearts']);
// OK, restituisce {'suit' => Suit::Hearts}

Poiché l'elemento implementa tutta l'interfaccia, funziona automaticamente anche dentro processMultiple(): il Processor chiama il suo metodo merge() proprio come per qualsiasi altro elemento.