Esquema Nette
Uma biblioteca prática para validação e normalização de estruturas de dados em relação a um determinado esquema com uma API inteligente e fácil de entender.
A documentação pode ser encontrada no site.
Instalação:
composer require nette/schema
Requer PHP versão 8.1 e suporta PHP até 8.4.
Você gosta de Nette Schema? Você está ansioso pelos novos recursos?
Obrigado!
Na variável $schema
temos um esquema de validação (o que exatamente isso significa e como criá-lo diremos mais tarde) e na variável $data
temos uma estrutura de dados que queremos validar e normalizar. Podem ser, por exemplo, dados enviados pelo usuário através de uma API, arquivo de configuração, etc.
A tarefa é manipulada pela classe NetteSchemaProcessor, que processa a entrada e retorna dados normalizados ou lança uma exceção NetteSchemaValidationException em caso de 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 um array de todas as strings de mensagens e $e->getMessageObjects()
retorna todas as mensagens como objetos NetteSchemaMessage.
E agora vamos criar um esquema. A classe NetteSchemaExpect é usada para defini-lo, na verdade definimos as expectativas de como os dados devem ser. Digamos que os dados de entrada devam ser uma estrutura (por exemplo, um array) 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, it passes
A saída, ou seja, o valor $normalized
, é o objeto stdClass
. Se quisermos que a saída seja um array, adicionamos uma conversão ao esquema Expect::structure([...])->castTo('array')
.
Todos os elementos da estrutura são opcionais e possuem um valor padrão null
. Exemplo:
$ data = [
' refundAmount ' => 17 ,
];
$ normalized = $ processor -> process ( $ schema , $ data ); // OK, it passes
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
O fato do 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 se tornar obrigatório usando Expect::bool()->required()
. Alteramos o valor padrão para false
usando Expect::bool()->default(false)
ou brevemente usando Expect::bool(false)
.
E se quiséssemos aceitar 1
e 0
além dos booleanos? Em seguida 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 os princípios básicos de como o esquema é definido e como os elementos individuais da estrutura se comportam. Mostraremos agora quais todos os outros elementos podem ser usados na definição de um esquema.
Todos os tipos de dados PHP padrão 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 então todos os tipos suportados pelos Validadores via Expect::type('scalar')
ou abreviado Expect::scalar()
. Também nomes de classes ou interfaces são aceitos, por exemplo Expect::type('AddressEntity')
.
Você também pode usar a notação de união:
Expect:: type ( ' bool|string|array ' )
O valor padrão é sempre null
exceto para array
e list
, onde é um array vazio. (Uma lista é um array indexado em ordem crescente de chaves numéricas a partir de zero, ou seja, um array não associativo).
A matriz é uma estrutura muito geral; é mais útil especificar exatamente quais elementos ela pode conter. Por exemplo, um array cujos elementos só podem ser 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 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, então podemos escrever:
Expect:: arrayOf (Expect:: bool ())
O valor padrão é uma matriz vazia. Se você especificar um valor padrão e chamar mergeDefaults()
, ele será mesclado com os dados passados.
anyOf()
é um conjunto de valores ou esquemas que um valor pode ser. Veja como escrever um array de elementos que pode 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 uma matriz de valores, use o operador de descompactação anyOf(...$variants)
.
O valor padrão é null
. Use o método firstIsDefault()
para tornar o primeiro elemento o padrão:
// default is 'hello'
Expect:: anyOf (Expect:: string ( ' hello ' ), true , null )-> firstIsDefault ();
Estruturas são objetos com chaves definidas. Cada um desses pares chave => valor é chamado de "propriedade":
As estruturas aceitam arrays 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 null
. Você pode definir propriedades obrigatórias usando required()
:
$ schema = Expect:: structure ([
' required ' => Expect:: string ()-> required (),
' optional ' => Expect:: string (), // the default value is null
]);
$ processor -> process ( $ schema , [ ' optional ' => '' ]);
// ERROR: option 'required' is missing
$ processor -> process ( $ schema , [ ' required ' => ' foo ' ]);
// OK, returns {'required' => 'foo', 'optional' => null}
Se você não deseja gerar propriedades apenas com um valor padrão, use skipDefaults()
:
$ schema = Expect:: structure ([
' required ' => Expect:: string ()-> required (),
' optional ' => Expect:: string (),
])-> skipDefaults ();
$ processor -> process ( $ schema , [ ' required ' => ' foo ' ]);
// OK, returns {'required' => '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). 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: 'optional' expects to be string, null given.
$ processor -> process ( $ schema , [ ' nullable ' => null ]);
// OK, returns {'optional' => null, 'nullable' => null}
Por padrão, não pode haver itens extras nos dados de entrada:
$ schema = Expect:: structure ([
' key ' => Expect:: string (),
]);
$ processor -> process ( $ schema , [ ' additional ' => 1 ]);
// ERROR: Unexpected item 'additional'
O 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 descontinuar a propriedade usando o método deprecated([string $message])
. Os avisos de descontinuação são retornados 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"]
Use min()
e max()
para limitar o número de elementos dos arrays:
// array, at least 10 items, maximum 20 items
Expect:: array ()-> min ( 10 )-> max ( 20 );
Para strings, limite seu comprimento:
// string, at least 10 characters long, maximum 20 characters
Expect:: string ()-> min ( 10 )-> max ( 20 );
Para números, limite seu valor:
// integer, between 10 and 20 inclusive
Expect:: int ()-> min ( 10 )-> max ( 20 );
Claro, é possível mencionar apenas min()
, ou apenas max()
:
// string, maximum 20 characters
Expect:: string ()-> max ( 20 );
Usando pattern()
, você pode especificar uma expressão regular que toda a string de entrada deve corresponder (ou seja, como se estivesse envolvida em caracteres ^
a $
):
// just 9 digits
Expect:: string ()-> pattern ( ' d{9} ' );
Você pode adicionar quaisquer outras restrições usando assert(callable $fn)
.
$ countIsEven = fn ( $ v ) => count ( $ v ) % 2 === 0 ;
$ schema = Expect:: arrayOf ( ' string ' )
-> assert ( $ countIsEven ); // the count must be even
$ processor -> process ( $ schema , [ ' a ' , ' b ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' , ' b ' , ' c ' ]); // ERROR: 3 is not even
Ou
Expect:: string ()-> assert ( ' is_file ' ); // the file must exist
Você pode adicionar sua própria descrição para cada afirmação. Fará parte da mensagem de erro.
$ 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.
O método pode ser chamado repetidamente para adicionar múltiplas restrições. Pode ser misturado com chamadas para transform()
e castTo()
.
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 múltiplas transformações. Pode ser misturado com chamadas para assert()
e castTo()
. As operações serão executadas na ordem em que são 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 isso, a função recebe um objeto NetteSchemaContext 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 );
});
Os dados validados com sucesso podem ser lançados:
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 construtor ou uma classe com construtor. Se a classe não possui construtor, uma instância dela é criada e todos os elementos da estrutura sã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 único parâmetro para o construtor:
Expect:: string ()-> castTo (DateTime::class);
// creates new DateTime(...)
Antes da validação em si, os dados podem ser normalizados usando o método before()
. Como exemplo, vamos ter um elemento que deve ser um array de strings (ex. ['a', 'b', 'c']
), mas recebe entrada na forma de uma string abc
:
$ explode = fn ( $ v ) => explode ( ' ' , $ v );
$ schema = Expect:: arrayOf ( ' string ' )
-> before ( $ explode );
$ normalized = $ processor -> process ( $ schema , ' a b c ' );
// OK, returns ['a', 'b', 'c']
Você pode gerar esquema de estrutura a partir da classe. Exemplo:
class Config
{
public string $ name ;
public ? string $ 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}
Classes anônimas também são suportadas:
$ schema = Expect:: from ( new class {
public string $ name ;
public ? string $ password ;
public bool $ admin = false ;
});
Como as informações obtidas na definição da classe podem não ser suficientes, você pode adicionar um esquema customizado para os elementos com o segundo parâmetro:
$ schema = Expect:: from ( new Config , [
' name ' => Expect:: string ()-> pattern ( ' w:.* ' ),
]);