Dentro de sus muchas y variadas características, PHP permite al desarrollador personalizar la presentación en pantalla de los eventos de error. Pero me estoy adelantando un poco. Rebobinemos y vayamos un poco más atrás...
Durante la ejecución de tu script hay mensajes de error que aparecen en pantalla cuando menos lo esperas, mayormente como respuesta a un evento causado por descuido en el manejo de las variables, objetos o funciones. Un evento de error común ocurre cuando se intenta incrementar el valor de una variable no inicializada, algo como esto:
$variable_not_declared ++;
En consecuencia se genera un evento de error, una advertencia PHP que se manifiesta en pantalla en la forma de un mensaje como el siguiente:
Warning: Undefined variable $variable_not_declared in C:\...\x.php on line 17
Este comportamiento puede variar ya sea que configuremos PHP para que muestre todos estos eventos, solamente algunos o que los ignore por completo. Esto puede hacerse directamente en sus directivas de arranque en el archivo php,ini o mediante el uso de la función ini_set() en cada script.
; Configuración en el archivo php.ini error_reporting = E_ALL
// Modificación del atributo directamente en el script // (En este ejemplo, reporta todos los eventos excepto los PHP Notice) error_reporting(E_ALL ^ E_NOTICE);
En cualquier caso, el ideal cuando ejecutamos en un entorno de Producción es que estos mensajes queden ocultos, que no se visualicen en pantalla, porque podrían revelar información sensible. Sin embargo, cuando estamos trabajando en un entorno de Desarrollo, creando, actualizando y/o corrigiendo nuestros scripts, es necesario, imperativo, que estos errores se visualicen en pantalla para que podamos corregirlos debidamente, como debe ser.
¿Y cómo podemos hacer que este manejo de errores se comporte de una forma u otra sea que esté en entorno de Producción o de Desarrollo? Bueno, es aquí que podemos aprovechar la personalización del manejo de errores que permite PHP.
¿Qué tipos de errores existen en PHP?
Hay tres tipos regulares de eventos de error que pueden ocurrir en PHP, a saber:
- Errores de sistema, como el que vimos antes. Ocurren durante la ejecución del script, por ejemplo al usar mkdir() para crear un directorio en una ruta no valida o sin los permisos necesarios. Muchos de estos eventos no interrumpen la ejecución del script aunque pueden conducir a comportamientos no deseados, en especial si no se les da el tratamiento adecuado.
Warning: mkdir(): No such file or directory in C:\...\x.php on line 48
Fatal error: Uncaught DivisionByZeroError: Division by zero in C:\...\x.php on line 6
trigger_error('No pudo rotar el log de errores', E_USER_WARNING);
Warning: No pudo rotar el log de errores in C:\...\x.php on line 283
Técnicamente existe un tipo de error adicional, un error fatal que a diferencia de los indicados arriba no puede ser capturado o interceptado para su personalización, porque ocurre durante la interpretación del script, antes de ser compilado y ejecutado, causados por mala sintaxis en el código.
Parse error: syntax error, unexpected end of file in C:\...\x.php on line 470
Aclarado este punto, comencemos ahora si con la personalización del manejo de errores.
¿Cómo personalizar los errores en PHP?
Para comenzar, vamos a crear una clase que nos permita encapsular su manejo. Si te preguntas si es necesario usar clases, la respuesta es “no”, no es obligatorio, pero el uso de clases nos permite mantener debidamente encapsulado y organizado el código que vamos a usar, tal como mandan “los de arriba”, ya sabes.
class ErrorHandler { ... }
A continuación, veamos que tanto debemos colocar dentro de nuestra clase.
Al momento de crear el objeto, vamos a indicarle que haga dos cosas:
- Forzar el registro de eventos en el log de errores de PHP (cualquiera que este sea).
- Prevenir que se generen eventos para el mismo error (por ejemplo cuando el evento ocurre dentro de un ciclo while o for).
public function __construct() { // Registrar errores en un archivo ini_set('log_errors', '1'); // Previene repita errores (mismo archivo, línea y mensaje de error) ini_set('ignore_repeated_errors', '1'); }
Pasamos ahora a definir un método watch(), que usaremos para activar el uso personalizado de errores (aclaro que podría hacerse este paso durante la creación del objeto, pero prefiero tenerlo aparte, pero dejo a tu propio criterio y diseño cómo prefieres usarlo).
public function watch(int $error_level = E_ALL) { error_reporting($error_level); // Bloquea salidas a pantalla de mensajes de error ini_set("display_errors", "off"); // Registra funciones a usar para despliegue de errores set_error_handler([$this, 'showError']); set_exception_handler([$this, 'showException']); }
Como se aprecia, el método habilita el reporte de ciertos tipos de errores y deshabilita por defecto la visualización automática de los mismos en pantalla ("display_errors" = "off"), de forma que la única salida a pantalla sea la provista por la personalización que vamos a realizar y que se consigue mediante el uso de las funciones set_error_handler() y set_exception_handler(). Las dos funciones reciben como argumento la función a ejecutar cuando se presente el evento, ya se trate de un error o de una excepción. Como vamos a usar uno de los métodos declarados en nuestra clase (métodos que deben declararse como públicos), el argumento se declara en la forma de un arreglo del tipo [(objeto), '(nombre del método asociado)'], tal como vemos en el código de arriba.
Veamos en detalle cada una de estas funciones.
set_error_handler
Nos permite registrar una función a invocarla cada que se genere un evento de error, ya sea nativo del sistema de PHP o sea un error manualmente “disparado” por el usuario a través de la función trigger_error().
La función registrada debe hacer uso de los siguientes argumentos para recuperar los datos del error (siempre en el orden descrito a continuación):
- El tipo de error (valor numérico). Véase la documentación del manual de PHP sobre las constantes de error disponibles.
- El mensaje de error.
- El archivo en el que se produjo el error (path completo).
- El número de línea en el que se produjo el error
Teniendo estas consideraciones en cuenta, el método a implementar para atender los errores deberá ser similar a este:
public function showError(int $type, string $message, string $file, int $line) { if (!error_reporting() & $type) { // Este código de error no está incluido en error_reporting, // ignora su presentación a pantalla. return; } // Captura traza de eventos $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); // Visualiza mensaje de error $this->viewError($type, $message, $file, $line, $trace); }
Es muy importante anotar que esta función será invocada siempre para todos los eventos de error. Así las cosas, para evitar aquellos mensajes no deseados y configurados al arrancar, se debe validar cuáles ignorar, lo que hace precisamente en la primera parte con la línea (!error_reporting() & $type) .
Importante: Si durante la atención a un evento de error se genera en esta función un error (ya sea propio de PHP o generado manualmente por el usuario), no se “disparará” un nuevo evento de error. De ser necesario, puede validarse la ocurrencia de tales errores con la función error_get_last(), que retorna un arreglo con los datos del error más reciente que no haya sido ya atendido o null si no existe ninguno, como veremos más adelante al definir el método viewError().
set_exception_handler
Esta función registra la función a usar para atender las excepciones ocurridas que no se encuentre dentro de un bloque try/catch. A diferencia del anterior, la función asociada recibirá como argumento un objeto con los datos de la excepción generada. Estos objetos son del tipo Exception (o una extensión hija de la misma clase) o un objeto del tipo Error. Este último se presenta cuando ocurre un error regular (no uno del tipo Exception) durante su ejecución. En este caso, si se genera un llamado a la rutina de manejo de excepciones para su atención.
public function showException(\Exception|\Error $e) { // Recupera los elementos básicos $type = $e->getCode(); $message = $e->getMessage(); $file = $e->getFile(); $line = $e->getLine(); // Traza actual $trace = $e->getTrace(); // Si no recupera la traza, intenta manualmente if (empty($trace)) { $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); } // Visualiza mensaje de error $this->viewError($type, $message, $file, $line, $trace); }
Al igual que sucede con los errores regulares, que pueden dispararse manualmente usando trigger_error(), las excepciones pueden también “arrojarse” manualmente al sistema así::
throw new Exception('Exception manualmente generada', 30);
Presentación del mensaje de error en pantalla
Nos queda por definir el método que en últimas presenta el mensaje de error en pantalla. Puede ser tan elaborado como se quiera, incluir registro del evento en base de datos o enviar una alerta por correo electrónico. Las posibilidades son tan amplias como las potenciales necesidades. Para efectos de ilustración vamos a definir una con un alcance básico, como esta:
private function viewError(int $type, string $message, string $file, int $line, array $trace) { // Mensaje de error (ejemplo básico) $msg_log= "<b>ERROR {$type}:</b> {$message} en \"{$file}\" línea {$line}"; // Registra evento en el log de errores (sin tags HTML) error_log(strip_tags($msg_log)); // Imprime mensaje de error en pantalla echo $msg_log; // Imprime traza del error print_r($trace); // Valida si ocurrió algún nuevo error $last_error = error_get_last(); if (!is_null($last_error)) { error_clear_last(); $this->showError(...$last_error); } }
Importante: Cuando se personaliza el manejo de errores, PHP no se guardan automáticamente registros en el log de errores, por ello debe realizarse manualmente usando la función error_log().
Ya con la clase terminada y como seguramente has hecho la tarea de poner todo en su lugar y llenar cualquier vacío procedimental, procedemos a incluir en nuestros scripts las siguientes líneas de código:
$errors = new ErrorHandler(); $errors->watch();
De esta forma habilitamos en nuestras aplicaciones el manejo personalizado de errores y podemos darle forma a nuestro gusto.
¿Queda algo pendiente? Bueno, ya entrados en gastos...
Ingrediente extra: Log rotativo para monitoreo
No podíamos irnos sin adicionar la cereza del pastel.
Ya que estamos personalizando el manejo de errores y guardando toda la información en el log de errores, que tradicionalmente es un archivo en el servidor, es importante asegurarse que este archivo no crezca desproporcionadamente (créanme cuando les digo que he visto archivos de este tipo tragarse por completo el espacio en disco del servidor). ¿Por que no aprovechar entonces esta clase para incluir una funcionalidad extra que valide el tamaño del archivo y lo renombre cada que supere cierto tamaño? Eso mismo digo, vamos a por ello.
// Propiedad pública usada para asignar el tope para la rotación public $sizeErrorLog = 0; private function rotateLog() { // Si no asigna tamaño de control, no tiene nada qué realizar if ($this->sizeErrorLog <= 0) { return; } $filename = trim(ini_get('error_log')); if ( $filename !== '' && is_file($filename) && filesize($filename) > $this->sizeErrorLog ) { // Renombra y mantiene solamente un histórico $old_name = $info['dirname'] . DIRECTORY_SEPARATOR . $info['filename'] . '(old).' . $info['extension']; if (@rename($filename, $old_name)) { // Reporta rotación del archivo error_log("ROTATE $old_name"); } } }
Y finalmente incluimos este método en el __construct() de la clase o lo habilitamos para que pueda ser invocado manualmente, ya ese detalle lo dejo a la elección de tu preferencia.
Con esto podemos dar por terminado esta revisión al manejo de errores de PHP.
¿Hay un repositorio de ejemplo completo?
Ciertamente lo hay. Este repositorio en Github contiene una clase que se basa en los principios descritos en este artículo, pero además se complementa con algunas de las siguientes características:
- Permite asignar diferentes rutinas de presentación para la misma clase, implementando una clase tipo interfaz para tal fin (RenderErrorInterface) y apoyándose en el manejo de vistas que exploramos en el artículo anterior.
- Provee un ejemplo donde se diferencia la visualización de errores ya sea que se configure en modo Producción o Desarrollo.
- Proporciona un control adicional para prevenir el registro de errores ocurridos en el mismo archivo y línea (como mencioné anteriormente, un error en un ciclo while() puede ser un verdadero dolor de cabeza para su depuración si imprime en pantalla y guarda en el registro de errores, cada mensaje de error que se presente).
- Como complemento al anterior, limita la cantidad de mensajes a mostrar antes de abortar el script.
- Permite guardar históricos ilimitados del log de errores, aunque la verdad no recomiendo est a práctica a menos que tengas una tarea que elimine los archivos más antiguos periódicamente. Sin embargo, puede ser útil para casos específicos y por breves períodos de tiempo.
Lo mejor queda para el final. Puedes consultar una demo en línea donde se puede ver esta clase en acción:
Demo funcional de la clase ErrorHandler
Y con esto podemos ahora si despedirnos, al menos por ahora.
Te agradezco si en la sección de Comentarios compartes tu experiencia con el manejo personalizado de los mensajes de error en PHP y cómo ha podido eso ayudarte a la hora de depurar y mejorar tu código.
Nos vemos próximamente.
Imagen de portada de Hans por cortesía de Pixabay
Comentarios
Publicar un comentario