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
.
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}
A matriz de todas as propriedades da estrutura é retornada pelo método getShape()
.
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
Você pode criar uma nova estrutura derivando de outra usando extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Matriz
Uma matriz com chaves definidas. Aplicam-se as mesmas regras das estruturas.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
Você também pode definir uma matriz indexada, conhecida como tupla:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
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:.*'),
]);