Schema: Validación de datos
Una biblioteca práctica para la validación y normalización de estructuras de datos con respecto a un esquema dado con una API inteligente y fácil de entender.
Instalación:
composer require nette/schema
Uso básico
En la variable $schema
tenemos un esquema de validación (qué significa esto exactamente y cómo crearlo lo
diremos más adelante) y en la variable $data
tenemos una estructura de datos que queremos validar y normalizar.
Pueden ser, por ejemplo, datos enviados por el usuario a través de una API, fichero de configuración, etc.
De esta tarea se encarga la clase Nette\Schema\Processor, que procesa la entrada y devuelve los datos normalizados o lanza una excepción Nette\Schema\ValidationException 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 un array con todas las cadenas de mensajes y
$e->getMessageObjects()
devuelve todos los mensajes como objetos Nette\Schema\Message.
Definición del esquema
Y ahora vamos a crear un esquema. La clase Nette\Schema\Expect se utiliza para definirlo, en realidad
definimos las expectativas de cómo deben ser los datos. Digamos que los datos de entrada deben ser una estructura (por ejemplo un
array) que contenga 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 ves por primera vez.
Enviemos los siguientes datos para su validación:
$data = [
'processRefund' => true,
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, pasa
La salida, es decir, el valor $normalized
, es el objeto stdClass
. Si queremos que la salida sea un
array, añadimos un cast al esquema Expect::structure([...])->castTo('array')
.
Todos los elementos de la estructura son opcionales y tienen un valor por defecto null
. Ejemplo:
$data = [
'refundAmount' => 17,
];
$normalized = $processor->process($schema, $data); // OK, pasa
// $normalized = {'processRefund' => null, 'refundAmount' => 17}
El hecho de que el valor por defecto sea null
no significa que se aceptaría en los datos de entrada
'processRefund' => null
. No, la entrada debe ser booleana, es decir, sólo true
o false
.
Tendríamos que permitir explícitamente null
a través de Expect::bool()->nullable()
.
Un elemento puede hacerse obligatorio mediante Expect::bool()->required()
. Cambiamos el valor por defecto a
false
utilizando Expect::bool()->default(false)
o en breve utilizando
Expect::bool(false)
.
¿Y si quisiéramos aceptar 1
and 0
además de booleanos? Entonces 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 ya sabes lo básico 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.
Tipos de datos: type()
Todos los tipos de datos estándar de PHP pueden ser listados 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 soportados por los
Validadores a través de Expect::type('scalar')
o abreviado Expect::scalar()
. También se aceptan
nombres de clases o interfaces, por ejemplo Expect::type('AddressEntity')
.
También se puede utilizar la notación de unión:
Expect::type('bool|string|array')
El valor por defecto es siempre null
excepto para array
y list
, donde es un array
vacío. (Una lista es una matriz indexada en orden ascendente de claves numéricas a partir de cero, es decir, una matriz no
asociativa).
Matriz de valores: arrayOf() listOf()
El array es una estructura demasiado general, es más útil especificar exactamente qué elementos puede contener. Por ejemplo, un array 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 no es una cadena
El segundo parámetro puede utilizarse 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' no es int
La lista es una matriz indexada:
$schema = Expect::listOf('string');
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 123]); // ERROR: 123 no es una cadena
$processor->process($schema, ['key' => 'a']); // ERROR: no es una lista
$processor->process($schema, [1 => 'a', 0 => 'b']); // ERROR: no es una lista
El parámetro también puede ser un esquema, por lo que podemos escribir:
Expect::arrayOf(Expect::bool())
El valor por defecto es un array vacío. Si especifica el valor por defecto, se fusionará con los datos pasados. Esto puede
desactivarse utilizando mergeDefaults(false)
(desde la versión 1.1).
Enumeración: anyOf()
anyOf()
es un conjunto de valores o esquemas que un valor puede ser. A continuación se muestra 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 no debe estar ahí
Los elementos de la 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 array. Para pasarle un array de
valores, utilice el operador de desempaquetado anyOf(...$variants)
.
El valor por defecto es null
. Utilice el método firstIsDefault()
para que el primer elemento sea el
valor predeterminado:
// default is 'hello'
Expect::anyOf(Expect::string('hello'), true, null)->firstIsDefault();
Estructuras
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
.
Por defecto, todas las propiedades son opcionales y tienen un valor por defecto de null
. Puede definir propiedades
obligatorias utilizando required()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // el valor por defecto es null
]);
$processor->process($schema, ['optional' => '']);
// ERROR: falta la opción 'required
$processor->process($schema, ['required' => 'foo']);
// OK, devuelve {'required' => 'foo', 'optional' => null}
Si no desea mostrar propiedades con un valor predeterminado, utilice skipDefaults()
:
$schema = Expect::structure([
'required' => Expect::string()->required(),
'optional' => Expect::string(),
])->skipDefaults();
$processor->process($schema, ['required' => 'foo']);
// OK, devuelve {'required' => 'foo'}
Aunque null
es el valor por defecto 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 utilizando
nullable()
:
$schema = Expect::structure([
'optional' => Expect::string(),
'nullable' => Expect::string()->nullable(),
]);
$processor->process($schema, ['optional' => null]);
// ERROR: 'optional' espera ser string, dado null.
$processor->process($schema, ['nullable' => null]);
// OK, devuelve {'optional' => null, 'nullable' => null}
El array de todas las propiedades de la estructura es devuelto por el método getShape()
.
Por defecto, 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'
Lo cual 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 crear una nueva estructura derivando de otra mediante extend()
:
$dog = Expect::structure([
'name' => Expect::string(),
'age' => Expect::int(),
]);
$dogWithBreed = $dog->extend([
'breed' => Expect::string(),
]);
Matriz
Un array con claves definidas. Se aplican las mismas reglas que para las estructuras.
$schema = Expect::array([
'required' => Expect::string()->required(),
'optional' => Expect::string(), // default value is null
]);
También puedes definir una matriz indexada, conocida como tupla:
$schema = Expect::array([
Expect::int(),
Expect::string(),
Expect::bool(),
]);
$processor->process($schema, [1, 'hello', true]); // OK
Depreciaciones
Puedes eliminar una propiedad utilizando el método deprecated([string $message])
método. Los avisos de
desaprobación son devueltos por $processor->getWarnings()
:
$schema = Expect::structure([
'old' => Expect::int()->deprecated('El elemento %ruta% está obsoleto'),
]);
$processor->process($schema, ['old' => 1]); // OK
$processor->getWarnings(); // ["El elemento 'old' es obsoleto"]
Rangos: min() max()
Utilice min()
y max()
para limitar el número de elementos de las matrices:
// matriz, al menos 10 elementos, máximo 20 elementos
Expect::array()->min(10)->max(20);
Para cadenas, limita su longitud:
// cadena, de al menos 10 caracteres, máximo 20 caracteres
Expect::string()->min(10)->max(20);
Para los números, limita su valor:
// número entero, entre 10 y 20 inclusive
Expect::int()->min(10)->max(20);
Por supuesto, es posible mencionar sólo min()
, o sólo max()
:
// cadena, máximo 20 caracteres
Expect::string()->max(20);
Expresiones regulares: pattern()
Utilizando pattern()
, puede especificar una expresión regular con la que debe coincidir toda la cadena de
entrada (es decir, como si estuviera envuelta en caracteres ^
a $
):
// sólo 9 dígitos
Expect::string()->pattern('\d{9}');
Aserciones personalizadas: assert()
Puede añadir cualquier otra restricción utilizando assert(callable $fn)
.
$countIsEven = fn($v) => count($v) % 2 === 0;
$schema = Expect::arrayOf('string')
->assert($countIsEven); // la cuenta debe ser par
$processor->process($schema, ['a', 'b']); // OK
$processor->process($schema, ['a', 'b', 'c']); // ERROR: 3 no es par
O:
Expect::string()->assert('is_file'); // el archivo debe existir
Puede añadir su propia descripción para cada aserción. Formará parte del mensaje de error.
$schema = Expect::arrayOf('string')
->assert($countIsEven, 'Even items in array');
$processor->process($schema, ['a', 'b', 'c']);
// Fallo en la aserción "Even items in array" para el elemento con valor array.
El método puede llamarse repetidamente para añadir múltiples restricciones. Puede entremezclarse con llamadas a
transform()
y castTo()
.
Transformación: transform()
Los datos validados correctamente pueden modificarse mediante una función personalizada:
// conversion to uppercase:
Expect::string()->transform(fn(string $s) => strtoupper($s));
El método puede llamarse repetidamente para añadir múltiples transformaciones. Puede entremezclarse con llamadas a
assert()
y castTo()
. Las operaciones se ejecutarán en el orden en que se declaren:
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 sencillo y menos
redundante que encadenar transform()
y assert()
. Para ello, la función recibe un objeto Context con un método addError()
, que puede
utilizarse para añadir 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);
});
Casting: castTo()
Los datos validados correctamente pueden ser emitidos:
Expect::scalar()->castTo('string');
Además de los tipos nativos de PHP, también se puede hacer cast a clases. Distingue si se trata de una clase simple sin constructor o de 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: ...)
El moldeado combinado 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(...)
Normalización: before()
Antes de la validación propiamente dicha, los datos pueden normalizarse utilizando el método before()
. Como
ejemplo, tengamos un elemento que debe ser un array de cadenas (ej. ['a', 'b', 'c']
), pero que recibe la entrada en
forma de cadena a b c
:
$explode = fn($v) => explode(' ', $v);
$schema = Expect::arrayOf('string')
->before($explode);
$normalized = $processor->process($schema, 'a b c');
// OK, devuelve ['a', 'b', 'c']
Asignación a objetos: from()
Se puede generar esquema de estructura a partir de la clase. Ejemplo:
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}
También se admiten clases anónimas:
$schema = Expect::from(new class {
public string $name;
public ?string $password;
public bool $admin = false;
});
Dado que la información obtenida de la definición de la clase puede no ser suficiente, puede añadir un esquema personalizado para los elementos con el segundo parámetro:
$schema = Expect::from(new Config, [
'name' => Expect::string()->pattern('\w:.*'),
]);