dancasdev / schema-validator
Schema-based data validator. Clean, flexible, framework-agnostic.
Requires
- php: ^8.1
README
Librería PHP liviana (sin dependencias externas) que valida arreglos de datos contra un esquema declarativo. Responde una sola pregunta: "¿este arreglo de datos cumple con la estructura que espero?" — y te devuelve si es válido, los errores (si los hay) y solamente los datos que declaraste en el esquema.
Instalación
composer require dancasdev/schema-validator
PHP 8.1 o superior. No requiere extensiones adicionales.
Uso básico
1. Definir un esquema
use DancasDev\SchemaValidator\SchemaValidator; $esquema = [ 'nombre' => [ 'type' => 'string', 'rules' => ['required' => true, 'min_length' => 2], ], 'edad' => [ 'type' => 'int', 'rules' => ['required' => true, 'min' => 18, 'max' => 99], ], ];
Todo campo del esquema comparte estas propiedades:
| Propiedad | Obligatorio | Descripción |
|---|---|---|
type |
Sí | Tipo del campo. Ver Tipos de datos |
rules |
No | Reglas de validación. Dependen del tipo |
default |
No | Valor por defecto si el campo no está presente |
messages |
No | Mensajes de error personalizados para este campo |
Algunos tipos añaden claves propias del campo:
objectusaschemayarrayusaitem_schema. Se documentan en su sección correspondiente, ya que no son globales.
2. Validar datos
$validador = new SchemaValidator(); $datos = [ 'nombre' => 'Juan', 'edad' => 25, ]; $resultado = $validador->validate($datos, $esquema); if ($resultado->isValid()) { echo "¡Todo correcto!"; }
3. Leer el resultado
| Método | Descripción |
|---|---|
isValid() |
true si no hay errores |
errors() |
['ruta' => 'mensaje', ...] — todos los errores |
error('edad') |
Mensaje de un campo específico, o null si no tiene |
hasError('edad') |
true si ese campo tiene error |
data() |
Arreglo con los datos resueltos (solo campos válidos) |
get('a.b') |
Valor de un campo por ruta con notación de puntos |
toArray() |
['valid' => bool, 'errors' => [...], 'data' => [...]] |
$resultado->errors(); // ['ruta' => 'mensaje', ...] $resultado->error('edad'); // mensaje de un campo específico $resultado->hasError('edad'); // true si ese campo tiene error $resultado->data(); // arreglo con los datos limpios $resultado->get('nombre'); // valor de un campo específico
4. Ejemplo completo
use DancasDev\SchemaValidator\SchemaValidator; $esquema = [ 'usuario' => [ 'type' => 'string', 'rules' => ['required' => true, 'min_length' => 3, 'max_length' => 20], ], 'email' => [ 'type' => 'string', 'rules' => ['required' => true, 'pattern' => '/^[^@]+@[^@]+\.[^@]+$/'], ], 'edad' => [ 'type' => 'int', 'rules' => ['required' => true, 'min' => 13], ], 'roles' => [ 'type' => 'array', 'rules' => ['item_type' => 'object'], 'item_schema' => [ 'nombre' => ['type' => 'string', 'rules' => ['required' => true]], ], ], ]; $validador = new SchemaValidator([], [ 'required' => 'El campo es obligatorio.', 'string' => 'Debe ser texto.', ]); $resultado = $validador->validate($datos, $esquema); if (!$resultado->isValid()) { foreach ($resultado->errors() as $ruta => $mensaje) { echo "{$ruta}: {$mensaje}\n"; } } $datosLimpios = $resultado->data();
Tipos de datos
Cada tipo define sus propias reglas (dentro de rules). Algunos añaden
además claves extra a nivel del campo (junto a type/rules).
string
'correo' => [ 'type' => 'string', 'rules' => [ 'min_length' => 5, 'max_length' => 100, 'enum' => ['admin', 'user', 'guest'], // valores permitidos 'pattern' => '/^[^@]+@[^@]+\.[^@]+$/', // expresión regular ], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
min_length |
int | Longitud mínima |
max_length |
int | Longitud máxima |
enum |
array | Valores permitidos (comparación estricta) |
pattern |
string | Expresión regular, evaluada con preg_match |
min_length/max_length cuentan caracteres (multibyte si ext-mbstring está
cargado; bytes en caso contrario). No se requieren extensiones.
int
'edad' => [ 'type' => 'int', 'rules' => ['required' => true, 'min' => 0, 'max' => 150], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
min |
int | Valor mínimo |
max |
int | Valor máximo |
float
'precio' => [ 'type' => 'float', 'rules' => ['min' => 0.01, 'max' => 9999.99], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
min |
float | Valor mínimo |
max |
float | Valor máximo |
number (acepta int y float)
'total' => [ 'type' => 'number', 'rules' => ['min' => 0, 'max' => 100000], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
min |
number | Valor mínimo |
max |
number | Valor máximo |
bool
'activo' => [ 'type' => 'bool', 'rules' => ['required' => true], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
array (lista)
array = lista secuencial. Los índices deben ser 0, 1, 2, .... Un array
asociativo NO es array — use el tipo object.
'etiquetas' => [ 'type' => 'array', 'rules' => [ 'min_length' => 1, 'max_length' => 10, 'item_type' => 'string', // tipo de cada elemento 'set' => ['a', 'b', 'c'], // valores permitidos ], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
min_length |
int | Cantidad mínima de elementos |
max_length |
int | Cantidad máxima de elementos |
item_type |
string | Tipo de cada elemento |
set |
array | Valores permitidos |
Valores de item_type: string, int, float, number, bool, array
(cada elemento es una lista) y object (cada elemento es cualquier array).
| Clave extra | Tipo | Descripción |
|---|---|---|
item_schema |
array | Esquema para cada elemento. Requiere item_type => 'object' |
Array de objetos (item_schema)
'empleados' => [ 'type' => 'array', 'rules' => ['item_type' => 'object'], 'item_schema' => [ 'nombre' => ['type' => 'string', 'rules' => ['required' => true]], 'edad' => ['type' => 'int', 'rules' => ['min' => 0]], ], ]
Cada elemento se valida contra item_schema. Los errores usan la ruta con el
índice del elemento: empleados.0.nombre, empleados.1.edad, etc.
object (cualquier array)
object = cualquier array (asociativo o lista). La distinción con array
es que array solo acepta listas.
'meta' => [ 'type' => 'object', 'rules' => ['required' => true], 'schema' => [ 'clave' => ['type' => 'string', 'rules' => ['required' => true]], 'valor' => ['type' => 'string', 'rules' => []], ], ]
| Regla | Tipo | Descripción |
|---|---|---|
required |
bool | Campo obligatorio |
| Clave extra | Tipo | Descripción |
|---|---|---|
schema |
array | Sub-esquema de las propiedades del objeto |
Objetos anidados
Con schema, cada propiedad se valida recursivamente y los errores se
registran con ruta punteada (meta.clave, meta.otra.prop).
Tipos múltiples (type como array)
Puedes especificar varios tipos aceptables. El validador prueba cada uno y si al menos uno pasa, el campo es válido. Las reglas se evalúan solo para el tipo que coincidió — reglas que no aplican se ignoran.
$esquema = [ 'id' => [ 'type' => ['string', 'int'], // acepta string o int 'rules' => [ 'min_length' => 1, // solo aplica si es string 'min' => 0, // solo aplica si es int ], ], ];
| Valor | ¿String? | ¿Int? | Resultado |
|---|---|---|---|
"abc" |
✅ pasa | ❌ no es int | ✅ válido |
42 |
❌ no es string | ✅ pasa (min=0) | ✅ válido |
-1 |
❌ no es string | ❌ falla min | ❌ inválido |
3.14 |
❌ no es string | ❌ no es int | ❌ inválido |
NULL como tipo permitido
Para campos que acepten explícitamente null, incluye 'NULL' en el array de
tipos:
$esquema = [ 'descuento' => [ 'type' => ['number', 'NULL'], // number o null 'rules' => ['min' => 0, 'max' => 100], ], ];
| Valor | Resultado |
|---|---|
15 |
✅ válido |
null |
✅ válido (NULL en types) |
"texto" |
❌ inválido |
Anidamiento en multi-type
object y array con schema/item_schema también funcionan en multi-type:
el anidamiento se aplica cuando el valor es un array. Un valor que no es array
cae a los demás tipos de la lista.
$esquema = [ 'meta' => [ 'type' => ['object', 'NULL'], // objeto o null 'rules' => [], 'schema' => [ 'clave' => ['type' => 'string', 'rules' => ['required' => true]], ], ], ];
| Valor | Resultado |
|---|---|
['clave' => 'x'] |
✅ valida el sub-esquema |
null |
✅ válido (NULL en types) |
['clave' => ''] |
❌ inválido (hijo falla) |
Comportamiento
Datos limpios
La librería solo devuelve los campos declarados en el esquema. Cualquier campo extra en los datos de entrada es ignorado:
$resultado = $validador->validate( ['nombre' => 'Juan', 'extra' => 'esto será ignorado'], ['nombre' => ['type' => 'string', 'rules' => []]], ); $resultado->data(); // ['nombre' => 'Juan'] ← sin 'extra'
data() solo incluye campos que pasaron la validación. Un campo inválido
(o un objeto/array con algún hijo inválido) queda fuera:
$resultado = $validador->validate( ['edad' => 'no-soy-numero'], ['edad' => ['type' => 'int', 'rules' => []]], ); $resultado->data(); // [] ← 'edad' falló y no se incluye
Valores por defecto
Cuando un campo no está presente en los datos, puedes asignarle un valor por defecto:
$esquema = [ 'rol' => [ 'type' => 'string', 'rules' => ['enum' => ['admin', 'user']], 'default' => 'user', ], ]; $resultado = $validador->validate([], $esquema); $resultado->data()['rol']; // 'user'
El valor por defecto también pasa por validación. Si no cumple las reglas, se reporta como error.
Campos vacíos y null
- Un string vacío (
'') en un campo no requerido se ignora: no genera error y no entra adata(). - Un string vacío con
required: truesí genera error. nullen un campo no requerido se ignora igual; con'NULL'entypese valida explícitamente como permitido (ver Tipos múltiples).
Mensajes de error
Códigos y mensajes por defecto
Todos los errores se identifican por un código. Estos son los mensajes en inglés que se usan si no los personalizas:
| Código | Mensaje por defecto |
|---|---|
required |
Is required. |
object |
Must be an object. |
string |
Must be a string. |
string_min_length |
Must be at least {min} characters. |
string_max_length |
Must be at most {max} characters. |
string_enum |
Must be one of: {values}. |
string_pattern |
Format is invalid. |
int |
Must be an integer. |
int_min |
Must be at least {min}. |
int_max |
Must be at most {max}. |
float |
Must be a float. |
float_min |
Must be at least {min}. |
float_max |
Must be at most {max}. |
number |
Must be a number. |
number_min |
Must be at least {min}. |
number_max |
Must be at most {max}. |
bool |
Must be a boolean. |
array |
Must be an array. |
array_item_type |
Must be of type {param}. |
array_min_length |
Must have at least {min} items. |
array_max_length |
Must have at most {max} items. |
array_set |
Contains an invalid value. |
type_mismatch |
Must be a {types}. |
| (código desconocido) | Validation failed. |
Personalización
Por instancia — aplica a todos los campos de ese validador:
$validador = new SchemaValidator([], [ 'required' => 'Este campo es obligatorio.', 'string' => 'Debe ser texto.', 'int' => 'Debe ser un número entero.', ]);
Por campo — gana sobre la instancia:
$esquema = [ 'correo' => [ 'type' => 'string', 'rules' => ['required' => true], 'messages' => [ 'required' => 'El correo es obligatorio.', 'string' => 'El correo debe ser texto.', ], ], ];
Jerarquía de resolución
1. Mensaje del campo (field.messages)
2. Mensaje de la instancia (constructor)
3. Mensaje hardcodeado en inglés (fallback)
Placeholders
Los mensajes pueden incluir placeholders que se reemplazan con los valores de las reglas:
| Placeholder | Se reemplaza por |
|---|---|
{min} |
min_length o min |
{max} |
max_length o max |
{values} |
la lista de enum o set |
{param} |
item_type |
{types} |
la lista de tipos de type (multi-type) |
Tipos de validación personalizados
Registra un validador propio con addType y úsalo como un tipo más:
use DancasDev\SchemaValidator\SchemaValidator; $validador = new SchemaValidator(); $validador->addType('telefono', function (mixed $valor, array $reglas, string $ruta, callable $msg): ?string { if (!is_string($valor)) { return $msg('string'); } if (!preg_match('/^\+?\d{7,15}$/', $valor)) { return 'Debe ser un teléfono válido.'; } return null; // válido }); $resultado = $validador->validate( ['tel' => '+521234567890'], ['tel' => ['type' => 'telefono', 'rules' => ['required' => true]]] );
La firma del callable es fn(mixed $valor, array $reglas, string $ruta, callable $msg): ?string:
| Parámetro | Tipo | Descripción |
|---|---|---|
$valor |
mixed |
El valor del campo a validar |
$reglas |
array |
Las reglas de rules filtradas para este tipo (solo las que le aplican) |
$ruta |
string |
Ruta del campo (p. ej. usuario.nombre en anidados) |
$msg |
callable |
fn(string $codigo): string — resuelve el mensaje del código siguiendo la jerarquía field > instancia > fallback |
Valor de retorno:
null→ el valor es válido.string→ mensaje de error.
Tests
composer test # o: php test/run.php
Suite sin dependencias (asserts propios) que cubre todos los tipos, reglas,
anidamiento, multi-type, mensajes y ValidationResult.
Licencia
MIT