Esquema de Nette
Una biblioteca práctica para la validación y normalización de estructuras de datos según un esquema determinado con una API inteligente y fácil de entender.
La documentación se puede encontrar en el sitio web.
Instalación:
composer require nette/schema
Requiere PHP versión 8.1 y admite PHP hasta 8.4.
¿Te gusta Nette Schema? ¿Estás esperando las nuevas funciones?
¡Gracias!
En la variable $schema
tenemos un esquema de validación (qué significa exactamente esto y cómo crearlo lo diremos más adelante) y en la variable $data
tenemos una estructura de datos que queremos validar y normalizar. Estos pueden ser, por ejemplo, datos enviados por el usuario a través de una API, un archivo de configuración, etc.
La tarea es manejada por la clase NetteSchemaProcessor, que procesa la entrada y devuelve datos normalizados o genera una excepción NetteSchemaValidationException en caso de error.
$ processor = new Nette Schema Processor ;
try {
$ normalized = $ processor -> process ( $ schema , $ data );
} catch ( Nette Schema ValidationException $ e ) {
echo ' Data is invalid: ' . $ e -> getMessage ();
}
El método $e->getMessages()
devuelve una matriz de todas las cadenas de mensajes y $e->getMessageObjects()
devuelve todos los mensajes como objetos NetteSchemaMessage.
Y ahora creemos un esquema. La clase NetteSchemaExpect se utiliza para definirlo; en realidad definimos las expectativas de cómo deberían verse los datos. Digamos que los datos de entrada deben ser una estructura (por ejemplo, una matriz) que contenga los elementos processRefund
de tipo bool y refundAmount
de tipo int.
use Nette Schema Expect ;
$ schema = Expect:: structure ([
' processRefund ' => Expect:: bool (),
' refundAmount ' => Expect:: int (),
]);
Creemos que la definición del esquema parece clara, incluso si la ve por primera vez.
Enviemos los siguientes datos para su validación:
$ data = [
' processRefund ' => true ,
' refundAmount ' => 17 ,
];
$ normalized = $ processor -> process ( $ schema , $ data ); // OK, it passes
La salida, es decir, el valor $normalized
, es el objeto stdClass
. Si queremos que la salida sea una matriz, agregamos una conversión al esquema Expect::structure([...])->castTo('array')
.
Todos los elementos de la estructura son opcionales y tienen un valor predeterminado null
. Ejemplo:
$ data = [
' refundAmount ' => 17 ,
];
$ normalized = $ processor -> process ( $ schema , $ data ); // OK, it passes
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
El hecho de que el valor predeterminado sea null
no significa que se aceptará en los datos de entrada 'processRefund' => null
. No, la entrada debe ser booleana, es decir, solo true
o false
. Tendríamos que permitir explícitamente null
a través de Expect::bool()->nullable()
.
Un elemento puede hacerse obligatorio usando Expect::bool()->required()
. Cambiamos el valor predeterminado a false
usando Expect::bool()->default(false)
o brevemente usando Expect::bool(false)
.
¿Y si quisiéramos aceptar 1
y 0
además de los valores booleanos? Luego enumeramos los valores permitidos, que también normalizaremos a 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
Ahora conoce los conceptos básicos de cómo se define el esquema y cómo se comportan los elementos individuales de la estructura. Ahora mostraremos qué otros elementos se pueden utilizar para definir un esquema.
Todos los tipos de datos PHP estándar se pueden enumerar en el esquema:
Expect:: string ( $ default = null )
Expect:: int ( $ default = null )
Expect:: float ( $ default = null )
Expect:: bool ( $ default = null )
Expect::null()
Expect:: array ( $ default = [])
Y luego todos los tipos admitidos por los validadores a través de Expect::type('scalar')
o Expect::scalar()
abreviado. También se aceptan nombres de clases o interfaces, por ejemplo Expect::type('AddressEntity')
.
También puedes usar notación de unión:
Expect:: type ( ' bool|string|array ' )
El valor predeterminado siempre es null
excepto para array
y list
, donde es una matriz vacía. (Una lista es una matriz indexada en orden ascendente de claves numéricas desde cero, es decir, una matriz no asociativa).
La matriz tiene una estructura demasiado general, es más útil especificar exactamente qué elementos puede contener. Por ejemplo, una matriz cuyos elementos sólo pueden ser cadenas:
$ 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
El segundo parámetro se puede utilizar para especificar claves (desde la versión 1.2):
$ schema = Expect:: arrayOf ( ' string ' , ' int ' );
$ processor -> process ( $ schema , [ ' hello ' , ' world ' ]); // OK
$ processor -> process ( $ schema , [ ' a ' => ' hello ' ]); // ERROR: 'a' is not int
La lista es una 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
El parámetro también puede ser un esquema, por lo que podemos escribir:
Expect:: arrayOf (Expect:: bool ())
El valor predeterminado es una matriz vacía. Si especifica un valor predeterminado y llama mergeDefaults()
, se fusionará con los datos pasados.
anyOf()
es un conjunto de valores o esquemas que puede tener un valor. A continuación se explica cómo escribir una matriz de elementos que pueden ser 'a'
, true
o 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
Los elementos de enumeración también pueden ser esquemas:
$ schema = Expect:: listOf (
Expect:: anyOf (Expect:: string (), true , null ),
);
$ processor -> process ( $ schema , [ ' foo ' , true , null , ' bar ' ]); // OK
$ processor -> process ( $ schema , [ 123 ]); // ERROR
El método anyOf()
acepta variantes como parámetros individuales, no como una matriz. Para pasarle una matriz de valores, use el operador de descompresión anyOf(...$variants)
.
El valor predeterminado es null
. Utilice el método firstIsDefault()
para hacer que el primer elemento sea el predeterminado:
// default is 'hello'
Expect:: anyOf (Expect:: string ( ' hello ' ), true , null )-> firstIsDefault ();
Las estructuras son objetos con claves definidas. Cada uno de estos pares clave => valor se denomina "propiedad":
Las estructuras aceptan matrices y objetos y devuelven objetos stdClass
(a menos que lo cambie con castTo('array')
, etc.).
De forma predeterminada, todas las propiedades son opcionales y tienen un valor predeterminado de null
. Puede definir propiedades obligatorias 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}
Si no desea generar propiedades con solo un valor predeterminado, use skipDefaults()
:
$ schema = Expect:: structure ([
' required ' => Expect:: string ()-> required (),
' optional ' => Expect:: string (),
])-> skipDefaults ();
$ processor -> process ( $ schema , [ ' required ' => ' foo ' ]);
// OK, returns {'required' => 'foo'}
Aunque null
es el valor predeterminado de la propiedad optional
, no está permitido en los datos de entrada (el valor debe ser una cadena). Las propiedades que aceptan null
se definen 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}
De forma predeterminada, no puede haber elementos adicionales en los datos de entrada:
$ schema = Expect:: structure ([
' key ' => Expect:: string (),
]);
$ processor -> process ( $ schema , [ ' additional ' => 1 ]);
// ERROR: Unexpected item 'additional'
Que podemos cambiar con otherItems()
. Como parámetro especificaremos el 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
Puede desaprobar la propiedad utilizando el método deprecated([string $message])
. Los avisos de obsolescencia son devueltos 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"]
Utilice min()
y max()
para limitar el número de elementos de las matrices:
// array, at least 10 items, maximum 20 items
Expect:: array ()-> min ( 10 )-> max ( 20 );
Para cadenas, limite su longitud:
// string, at least 10 characters long, maximum 20 characters
Expect:: string ()-> min ( 10 )-> max ( 20 );
Para números, limite su valor:
// integer, between 10 and 20 inclusive
Expect:: int ()-> min ( 10 )-> max ( 20 );
Por supuesto, es posible mencionar solo min()
o solo max()
:
// string, maximum 20 characters
Expect:: string ()-> max ( 20 );
Usando pattern()
, puede especificar una expresión regular con la que toda la cadena de entrada debe coincidir (es decir, como si estuviera envuelta en caracteres ^
a $
):
// just 9 digits
Expect:: string ()-> pattern ( ' d{9} ' );
Puede agregar cualquier otra restricción 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
O
Expect:: string ()-> assert ( ' is_file ' ); // the file must exist
Puede agregar su propia descripción para cada afirmación. Será parte del mensaje de error.
$ 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.
El método se puede llamar repetidamente para agregar múltiples restricciones. Se puede mezclar con llamadas a transform()
y castTo()
.
Los datos validados exitosamente se pueden modificar usando una función personalizada:
// conversion to uppercase:
Expect:: string ()-> transform ( fn ( string $ s ) => strtoupper ( $ s ));
El método se puede llamar repetidamente para agregar múltiples transformaciones. Se puede mezclar con llamadas a assert()
y castTo()
. Las operaciones se ejecutarán en el orden en que sean declaradas:
Expect:: type ( ' string|int ' )
-> castTo ( ' string ' )
-> assert ( ' ctype_lower ' , ' All characters must be lowercased ' )
-> transform ( fn ( string $ s ) => strtoupper ( $ s )); // conversion to uppercase
El método transform()
puede transformar y validar el valor simultáneamente. Esto suele ser más simple y menos redundante que encadenar transform()
y assert()
. Para este propósito, la función recibe un objeto NetteSchemaContext con un método addError()
, que puede usarse para agregar información sobre problemas de validación:
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 );
});
Se pueden transmitir datos validados exitosamente:
Expect:: scalar ()-> castTo ( ' string ' );
Además de los tipos PHP nativos, también puedes transmitir a clases. Distingue si se trata de una clase simple sin constructor o una clase con constructor. Si la clase no tiene constructor, se crea una instancia de la misma y todos los elementos de la estructura se escriben en sus propiedades:
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
Si la clase tiene un constructor, los elementos de la estructura se pasan como parámetros con nombre al constructor:
class Info
{
public function __construct (
public bool $ processRefund ,
public int $ refundAmount ,
) {
}
}
// creates $obj = new Info(processRefund: ..., refundAmount: ...)
La conversión combinada con un parámetro escalar crea un objeto y pasa el valor como único parámetro al constructor:
Expect:: string ()-> castTo (DateTime::class);
// creates new DateTime(...)
Antes de la validación en sí, los datos se pueden normalizar utilizando el método before()
. Como ejemplo, tengamos un elemento que debe ser una matriz de cadenas (por ejemplo, ['a', 'b', 'c']
), pero recibe entrada en forma de cadena abc
:
$ explode = fn ( $ v ) => explode ( ' ' , $ v );
$ schema = Expect:: arrayOf ( ' string ' )
-> before ( $ explode );
$ normalized = $ processor -> process ( $ schema , ' a b c ' );
// OK, returns ['a', 'b', 'c']
Puede generar un esquema de estructura a partir de la clase. Ejemplo:
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}
También se admiten clases anónimas:
$ schema = Expect:: from ( new class {
public string $ name ;
public ? string $ password ;
public bool $ admin = false ;
});
Debido a que la información obtenida de la definición de clase puede no ser suficiente, puedes agregar un esquema personalizado para los elementos con el segundo parámetro:
$ schema = Expect:: from ( new Config , [
' name ' => Expect:: string ()-> pattern ( ' w:.* ' ),
]);