Ir al contenido principal

Manejo de errores personalizados en PHP

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
    
  • Errores fatales o Excepciones no capturadas, estas últimas ocurren cuando no se usan bloques try/catch apropiadamente. En general, estos eventos de error interrumpen el script tan pronto ocurren, causando una terminación abrupta. Un ejemplo clásico de este tipo de eventos ocurre al intentar realizar una división por cero.
  •  Fatal error: Uncaught DivisionByZeroError: Division by zero in C:\...\x.php on line 6
    
  • Errores de usuario. Estos son eventos de error generados directamente por el usuario, por ejemplo al validar la existencia de un archivo en disco. Dependiendo del tipo de error asignado (E_USER_ERROR, E_USER_WARNING o E_USER_NOTICE), puede o no terminar la ejecución inmediata del script (esto ocurre al generar eventos con tipo E_USER_ERROR).
  • 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).
De esta forma, aunque no hagamos más nada, podemos estar seguros que cualquier evento de error quedará debidamente registrado para su posterior estudio.
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

Entradas populares de este blog

Manejo de recursos HTML para tus páginas web con PHP

Déjame saber si te resulta familiar esta situación: páginas web que descargan el mismo recurso (sean estilos CSS o código Javascript) más de una vez o incluyen recursos remotos que tardan una eternidad en cada descarga. Yo lo he visto en más de una ocasión y no es difícil imaginar el porqué ocurre. Un desarrollador incluye el recurso de estilos que necesita su segmento de código y otro hace lo mismo, sin reparar (o sin que siquiera importe) que comparten el mismo recurso. En otro escenario muy común, acostumbran incluir muchos recursos remotos, con lo que el rendimiento de la página depende de lo rápido que responda dicho recurso. ¿Puede hacerse algo al respecto? Claro que si. Vamos a crear una clase en PHP que se encargue de administrar estos recursos y que nos facilite su despliegue en la página sin repeticiones . ¿Y respecto a la demora en la carga de recursos remotos? Atendamos una cosa por vez, porque como dicen por ahí: «Vísteme despacio, que tengo prisa». Administrando ...

Manejo de clases globales únicas en PHP

¿Cómo acceder desde cualquier script en tu proyecto a Clases y/o funciones de uso común? Este puede ser una de las primeras directrices a establecer para cualquier proyecto porque siempre, siempre , sea en  PHP  u otro lenguaje, será necesario usar recursos comunes. En PHP existen diferentes alternativas para su manejo, ya sea por medio de variables globales o de clases/objetos estáticos. A continuación consideraremos una propuesta para este manejo. Creación de recursos globales Para ilustrar esta solución, partimos de la necesidad de implementar una librería para manejo de servicios relacionados con el servidor Web, que de forma amigable nos permita disponer de información como: Valores almacenados de la variable superglobal $_SERVER de PHP. Valores asociados a la consulta realizada por el usuario, por Ej. la dirección IP del usuario o la URL ingresada. Valores asociados al servidor web usado, por Ej. la dirección IP del servidor o la ubicación del script que ej...

¿Qué tan bueno es realmente el “foreach” en PHP?

Como toda buena historia, esta comienza hace algún tiempo. El que fuera mi jefe por allá en la primera década del 2000, realmente odiaba (y mucho) el uso del foreach en el código PHP . Prefería que usáramos alguna alternativa diferente, alguna combinación del  for o del while . ¿Por qué? Ve tú a saber, nunca fue abierto respecto a las razones de su aprensión hacia ese constructor propio del lenguaje. Pero antes de continuar, veamos qué es y para qué nos puede servir. Arreglos, tenían que ser arreglos ¿Qué es foreach ? De acuerdo al manual de PHP , su definición es la siguiente: El constructor foreach proporciona un modo sencillo de iterar sobre arrays . foreach funciona sólo sobre arrays y objetos , y emitirá un error al intentar usarlo con una variable de un tipo diferente de datos o una variable no inicializada. Para su uso correcto existen dos sintaxis validas, a saber: foreach (expresión_array as $value) { ... } foreach (expresión_array as $key => $value) { ....