El bug silencioso
Un usuario no puede actualizar su perfil: escribe un 0 en el campo “Años de experiencia”, pulsa guardar, y la web no hace nada. Ni actualiza la base de datos, ni salta ningún error.
Abres el controlador esperando una lógica loca y rota y te encuentras esto:
if (!empty($yearsExperience)) {
$user->updateExperience($yearsExperience);
}
Para PHP, el número 0 (y hasta el string "0") cuenta como “vacío”. Le estás diciendo al servidor que si el usuario mete un cero, es como si no hubiera escrito nada. La función updateExperience nunca se ejecutará.
Esta confusión entre que algo exista y qué valor tiene es uno de los fallos de lógica más habituales en la historia de PHP. Vamos a intentar dejarlo claro con una tabla testeada en php.
Primero, ¿qué hacen isset() y empty()?
Antes vamos a ver lo básico. Las dos vienen de serie con PHP y sirven para lo mismo: preguntar algo sobre una variable sin que el programa pete si esa variable no existe. Las dos reciben una variable y te devuelven true o false:
isset($var) // ¿la variable existe y NO es null?
empty($var) // ¿la variable no existe, o su valor cuenta como "nada"?
isset()pregunta por la existencia: devuelvetruesi la variable está definida y su valor es distinto denull.empty()pregunta por el valor: devuelvetruesi la variable no existe o su contenido es uno de los valores que PHP considera “vacíos” (ahora vemos cuáles).
Sobre el papel parecen lo mismo con distinto nombre… y justo ahí empieza el lío, porque para un montón de valores las dos devuelven true a la vez. Vamos a verlo con un ejemplo.
El modelo mental: la caja y el contenido
Imagina una variable como si fuese una caja con una etiqueta:
isset()mira la caja: “¿existe una caja con esta etiqueta y lo de dentro es algo distinto denull?”. No le importa el valor. La caja puede contenerfalse,"",0o[]: mientras exista y no seanull,isset()devuelvetrue.empty()mira el contenido: “¿la caja no existe, o lo de dentro equivale a ‘la nada’?”. Y PHP es muy generoso con lo que considera “nada”.
Estos son los valores que empty() considera vacíos (los falsy falsy Valores que PHP convierte a false en un contexto booleano. empty() los trata a todos como vacíos. de PHP):
"" // string vacío
"0" // ¡el string cero también!
0 // entero cero
0.0 // float cero
null
false
[] // array vacío
AdvertenciaEl que pilla a todo el mundo es
"0". Un usuario que escribe0en un formulario manda el string"0", yempty("0")estrue. Por esoempty()es una trampa para cualquier campo donde el cero sea un valor legítimo: cantidades, contadores, años de experiencia, stock…Y el remate fino: de todos los “ceros en texto”, solo el
"0"pelado cuenta como vacío.empty("0.0")yempty("00")devuelvenfalse. Ni PHP se aclara del todo consigo mismo.
La tabla de verdad (comprobada en PHP 8.4)
Valor de | isset($x) | empty($x) | is_null($x) |
|---|---|---|---|
| No definida | false | true | ⚠️ Warning |
null | false | true | true |
| true | true | false |
| true | false | false |
| true | true | false |
| true | true | false |
false | true | true | false |
| true | true | false |
| true | false | false |
true | true | false | false |
NotaTanto
isset()comoempty()son construcciones del lenguaje construcciones del lenguaje Palabras reservadas del motor de PHP que se ejecutan a nivel de opcode, no como funciones normales. Por eso pueden recibir una variable que no existe sin lanzar error. , no funciones. Por eso pueden evaluar una variable no definida sin quejarse. En cambio, pasar una variable inexistente ais_null()(que sí es una función) lanza unWarning: Undefined variableen PHP 8, aunque luego devuelvatrue.
empty() NO es lo contrario de isset()
Es el error mental más común. Mirando la tabla: para "", 0, "0", false y [], tanto isset() como empty() devuelven true a la vez. No son opuestos. isset() pregunta por la existencia; empty() pregunta por el valor. Confundirlos es justo lo que provoca el bug del cero.
Dos trucos de isset() que casi nadie usa
Antes de ir a las trampas, dos comportamientos de isset() que te ahorran código y que mucha gente no conoce. Los dos comprobados en PHP 8.4:
1. Acepta varios argumentos a la vez (es un AND). isset($a, $b, $c) solo devuelve true si todas existen y ninguna es null. Así te ahorras encadenar comprobaciones:
// en vez de esto...
if (isset($_POST['nombre']) && isset($_POST['email']) && isset($_POST['edad'])) { }
// esto es equivalente
if (isset($_POST['nombre'], $_POST['email'], $_POST['edad'])) {
// los tres campos llegaron y ninguno es null
}
2. No revienta en arrays anidados. Comprobar isset($arr['a']['b']['c']) no lanza ningún warning aunque 'a' ni siquiera exista: si cualquier nivel intermedio falta, devuelve false y punto. Por eso es la herramienta perfecta para bucear en respuestas JSON profundas sin reventar:
$data = json_decode($respuesta, true);
// nada de warnings aunque falte 'usuario' o 'direccion'
if (isset($data['usuario']['direccion']['ciudad'])) {
$ciudad = $data['usuario']['direccion']['ciudad'];
}
ConsejoSu primo
??=(null coalescing assignment) asigna solo si la clave falta o esnull, respetando el0.$config['reintentos'] ??= 3deja intacto un0que ya estuviera puesto, pero rellena con3si la clave no existía. Justo lo contrario de lo que haría un?:con valores falsy.
Las trampas de producción más comunes
1. El dilema del PATCH en APIs
En una API REST, un PATCH actualiza solo algunos campos. Y hay una diferencia enorme entre dos situaciones:
- La clave no viene en el JSON → no toques ese campo.
- La clave viene como
"bio": null→ el usuario quiere borrar su biografía.
Si usas empty($data['bio']) (o isset()), tratas ambos casos igual y pierdes esa intención. La forma correcta es array_key_exists(), que sí distingue una clave presente con valor null de una clave ausente:
if (array_key_exists('bio', $data)) {
// el cliente envió la clave explícitamente (aunque sea null)
$user->setBio($data['bio']);
}
AdvertenciaY ojo, porque el operador
??tampoco te salva aquí: tratanulligual que una clave ausente. Tanto con['bio' => null]como con[], la expresión$data['bio'] ?? 'defecto'devuelve'defecto'. Para distinguir “vino null” de “no vino”, solo te valearray_key_exists().
2. La trampa del espacio en blanco
Un usuario mete espacios (" ") en un campo obligatorio. Como el string no está vacío, empty(" ") es false y el formulario se procesa con basura. Limpia siempre los laterales antes de validar:
$nombre = trim($_POST['name'] ?? '');
if ($nombre === '') {
// ahora sí: vacío de verdad
}
3. El silenciador de erratas
Como empty() no avisa cuando una clave no existe, es tentador usarlo para tragarse warnings. El problema es que entonces una errata pasa desapercibida:
if (!empty($config['usre_id'])) { // typo: usre_id en vez de user_id
// este bloque NUNCA se ejecuta, y PHP no te avisa
}
Estructura bien tus datos y accede con ?? solo donde de verdad sea opcional, en lugar de tapar errores con empty().
¿Cuál uso en cada caso?
Para comprobar que una clave o variable existe y no es null antes de leerla. Ideal para acceder a $_GET, $_POST o arrays sin provocar warnings.
Solo cuando de verdad quieras tratar 0, "0", "" y [] como "sin contenido" a la vez. Perfecto para "¿hay algo que mostrar en esta lista?", peligroso para validar formularios.
Cuando necesites distinguir una clave con valor null de una clave ausente. Imprescindible en APIs PATCH y actualizaciones parciales.
Para validar de verdad: $input === '' o $input === null. Sin sorpresas de conversión de tipos. Combínalo con trim() y filter_var().
Validación moderna en PHP 8+
Depender de la magia de los valores falsy es un antipatrón. Sé explícito:
- Usa el null coalescing ($var = $data['x'] ?? null) para leer opcionales sin warnings, y ??= para asignar por defecto solo si falta.
- Valida contra el valor real: $input === null || $input === '' (tras un trim()).
- Valida tipos con filter_var() (FILTER_VALIDATE_INT, FILTER_VALIDATE_EMAIL...) en lugar de adivinar.
- Usa array_key_exists() para actualizaciones parciales en APIs JSON.
- Usar empty() en campos donde 0 o "0" sean datos válidos (cantidades, stock, contadores).
- Tapar warnings de claves inexistentes con empty() en vez de estructurar bien los datos.
- Creer que empty() es el opuesto de isset(): para "", 0, "0", false y [] ambos dan true.
- Usar isset() para saber si un campo de texto se rellenó: un string vacío "" da true.
Croquetando
isset() comprueba si la variable está definida y no es null, y empty() mira el contenido (valor) de la variable. No son opuestos. La regla práctica: usa isset() (o ??) para leer datos opcionales sin warnings, evita empty() donde el cero sea legítimo, y tira de array_key_exists() cuando necesites saber si una clave vino aunque sea null.
ImportanteEl
0y el"0"sonempty(), así que no usesempty()para validar formularios con números.isset()no distingue""de “relleno”.??tratanullcomo ausente. Para validar de verdad:trim()+ comparación estricta (=== '',=== null) yfilter_var()para tipos.
EA! Nos vemos en los bares! 🍻