Schema: Validação de dados

Uma biblioteca prática para validação e normalização de estruturas de dados contra um determinado esquema com uma API inteligente e fácil de entender.

Instalação:

composer require nette/schema

Utilização básica

Na variável $schema temos um esquema de validação (o que exatamente isto significa e como criá-lo diremos mais tarde) e na variável $data temos uma estrutura de dados que queremos validar e normalizar. Estes podem ser, por exemplo, dados enviados pelo usuário através de uma API, arquivo de configuração, etc.

A tarefa é tratada pela classe Nette\Schema\Processor, que processa a entrada e ou retorna dados normalizados ou lança uma exceção Nette\Schema\ValidationException sobre erro.

$processor = new Nette\Schema\Processor;

try {
	$normalized = $processor->process($schema, $data);
} catch (Nette\Schema\ValidationException $e) {
	echo 'Data is invalid: ' . $e->getMessage();
}

O método $e->getMessages() retorna a matriz de todas as cadeias de mensagens e $e->getMessageObjects() retorna todas as mensagens como objetos “Nette\Schema\Message:https://api.nette.org/…Message.html ”.

Definindo o esquema

E agora vamos criar um esquema. A classe Nette\Schema\Expect é usada para defini-lo, na verdade definimos expectativas de como os dados devem ser. Digamos que os dados de entrada devem ser uma estrutura (por exemplo, uma matriz) contendo elementos processRefund do tipo bool e refundAmount do tipo int.

use Nette\Schema\Expect;

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

Acreditamos que a definição do esquema parece clara, mesmo que você a veja pela primeira vez.

Vamos enviar os seguintes dados para validação:

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

$normalized = $processor->process($schema, $data); // OK, ele passa

A saída, ou seja, o valor $normalized, é o objeto stdClass. Se quisermos que a saída seja uma matriz, adicionamos um elenco ao esquema Expect::structure([...])->castTo('array').

Todos os elementos da estrutura são opcionais e têm um valor padrão null. Exemplo:

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

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

O fato de o valor padrão ser null não significa que ele seria aceito nos dados de entrada 'processRefund' => null. Não, a entrada deve ser booleana, ou seja, apenas true ou false. Teríamos que permitir explicitamente null via Expect::bool()->nullable().

Um item pode ser tornado obrigatório usando Expect::bool()->required(). Mudamos o valor padrão para false usando Expect::bool()->default(false) ou em breve usando Expect::bool(false).

E se quiséssemos aceitar 1 and 0 além de booleanos? Então listamos os valores permitidos, que também normalizaremos para booleanos:

$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

Agora você conhece as bases de como o esquema é definido e como os elementos individuais da estrutura se comportam. Agora vamos mostrar o que todos os outros elementos podem ser usados na definição de um esquema.

Tipos de dados: tipo()

Todos os tipos de dados padrão PHP podem ser listados no esquema:

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

E depois todos os tipos suportados pelos validadores via Expect::type('scalar') ou abreviado Expect::scalar(). Também são aceitos nomes de classes ou interfaces, por exemplo Expect::type('AddressEntity').

Você também pode usar a notação sindical:

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

O valor padrão é sempre null exceto para array e list, onde é uma matriz vazia. (Uma lista é um array indexado em ordem ascendente de chaves numéricas a partir de zero, ou seja, um array não-associativo).

Conjunto de valores: arrayOf() listOf()

A matriz é estrutura muito geral, é mais útil especificar exatamente quais elementos ela pode conter. Por exemplo, uma matriz cujos elementos só podem ser cordas:

$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

O segundo parâmetro pode ser usado para especificar chaves (desde a versão 1.2):

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

$processor->process($schema, ['hello', 'world']); // OK
$processor->process($schema, ['a' => 'hello']); // ERROR: 'a' is not int

A lista é uma matriz indexada:

$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

O parâmetro também pode ser um esquema, para que possamos escrever:

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

O valor padrão é uma matriz vazia. Se você especificar o valor padrão, ele será fundido com os dados passados. Isto pode ser desabilitado usando mergeDefaults(false) (desde a versão 1.1).

Enumeração: anyOf()

anyOf() é um conjunto de valores ou esquemas que um valor pode ser. Eis como escrever um conjunto de elementos que podem ser 'a', true, ou 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

Os elementos de enumeração também podem ser esquemas:

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

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

O método anyOf() aceita variantes como parâmetros individuais, não como array. Para passar-lhe uma matriz de valores, use o operador de desembalagem anyOf(...$variants).

O valor padrão é null. Use o método firstIsDefault() para tornar o primeiro elemento o padrão:

// o padrão é 'olá'.
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();

Estruturas

As estruturas são objetos com chaves definidas. Cada uma destas chaves ⇒ pares de valores é chamada de “propriedade”:

As estruturas aceitam matrizes e objetos e retornam objetos stdClass (a menos que você altere com castTo('array'), etc.).

Por padrão, todas as propriedades são opcionais e têm um valor padrão de null. Você pode definir propriedades obrigatórias usando required():

$schema = Expect::structure([
	'required' => Expect::string()->required(),
	'optional' => Expect::string(), // o valor padrão é nulo
]);

$processor->process($schema, ['optional' => '']);
// ERROR: falta a opção 'necessário'.

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

Se você não quiser emitir propriedades com apenas um valor padrão, use skipDefaults():

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

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

Embora null seja o valor padrão da propriedade optional, ele não é permitido nos dados de entrada (o valor deve ser uma string). As propriedades que aceitam null são definidas usando nullable():

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

$processor->process($schema, ['optional' => null]);
// ERROR: 'opcional' espera ser string, dado nulo.

$processor->process($schema, ['nullable' => null]);
// OK, retorna {'opcional' => nulo, 'anulável' => nulo}

Por padrão, não pode haver itens extras nos dados de entrada:

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

$processor->process($schema, ['additional' => 1]);
// ERRO: Item inesperado 'adicional'.

Que podemos mudar com otherItems(). Como parâmetro, especificaremos o esquema para cada elemento extra:

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

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

Depreciações

Você pode depreciar os bens usando o deprecated([string $message]) método. Os avisos de depreciação são devolvidos por $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"]

Faixas: min() max()

Use min() e max() para limitar o número de elementos para arrays:

// matriz, pelo menos 10 itens, máximo 20 itens
Expect::array()->min(10)->max(20);

Para as cordas, limite seu comprimento:

// string, com pelo menos 10 caracteres, máximo 20 caracteres
Expect::string()->min(10)->max(20);

Para os números, limite seu valor:

// inteiro, entre 10 e 20 inclusive
Expect::int()->min(10)->max(20);

Naturalmente, é possível mencionar apenas min(), ou apenas max():

// string, máximo 20 caracteres
Expect::string()->max(20);

Expressões regulares: padrão()

Usando pattern(), você pode especificar uma expressão regular que a string de entrada whole deve combinar (ou seja, como se estivesse embrulhada em caracteres ^ a $):

// apenas 9 dígitos
Expect::string()->pattern('\d{9}');

Asserções personalizadas: assert()

Você pode adicionar quaisquer outras restrições usando assert(callable $fn).

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

$schema = Expect::arrayOf('string')
	->assert($countIsEven); // a contagem deve ser uniforme

$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 não é nem

Ou

Expect::string()->assert('is_file'); // o arquivo deve existir

Você pode acrescentar sua própria descrição para cada asserção. Ela será parte da mensagem de erro.

$schema = Expect::arrayOf('string')
	->assert($countIsEven, 'Even items in array');

$processor->process($schema, ['a', 'b', 'c']);
// afirmação falhada "Itens pares na matriz" para item com matriz de valores.

O método pode ser chamado repetidamente para adicionar várias restrições. Ele pode ser misturado com chamadas para transform() e castTo().

Transformação: transform()

Os dados validados com sucesso podem ser modificados usando uma função personalizada:

// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));

O método pode ser chamado repetidamente para adicionar várias transformações. Ele pode ser misturado com chamadas para assert() e castTo(). As operações serão executadas na ordem em que forem declaradas:

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

O método transform() pode transformar e validar o valor simultaneamente. Isso geralmente é mais simples e menos redundante do que encadear transform() e assert(). Para esse fim, a função recebe um objeto Context com um método addError(), que pode ser usado para adicionar informações sobre problemas de validação:

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

Transmissão: castTo()

Os dados validados com sucesso podem ser convertidos:

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

Além dos tipos nativos do PHP, você também pode converter para classes. Ele distingue se é uma classe simples sem um construtor ou uma classe com um construtor. Se a classe não tiver um construtor, será criada uma instância dela e todos os elementos da estrutura serão gravados em suas propriedades:

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

Se a classe tiver um construtor, os elementos da estrutura serão passados como parâmetros nomeados para o construtor:

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

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

A conversão combinada com um parâmetro escalar cria um objeto e passa o valor como o único parâmetro para o construtor:

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

Normalização: before()

Antes da validação propriamente dita, os dados podem ser normalizados usando o método before(). Como exemplo, vamos ter um elemento que deve ser um conjunto de cordas (por exemplo ['a', 'b', 'c']), mas recebe entrada sob a forma de um cordel a b c:

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

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

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

Mapeamento para objetos: from()

Você pode gerar um esquema de estrutura a partir da classe. Exemplo:

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}

Também há suporte para classes anônimas:

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

Como as informações obtidas da definição da classe podem não ser suficientes, você pode adicionar um esquema personalizado para os elementos com o segundo parâmetro:

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