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 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...

Caché de datos propios para agilizar ejecución de scripts PHP

De vez en cuando viene bien ayudar a PHP a generar respuestas de forma mucho más rápida y eficiente de lo que ya es capaz por si mismo. En mi opinión, la mejor forma de hacerlo es implementar un sistema de caché propio o (una opción un tanto más aburrida) reutilizar alguno ya existente. En este artículo detallaremos la implementación de una clase que permita este cometido. Aviso: Este artículo contiene ejemplos de programación en PHP aunque los conceptos explicados pueden ser aplicados a scripts realizados en cualquier otro lenguaje de programación. Para ilustrar este proceso, veamos el siguiente caso no tan hipotético: Una aplicación web hace uso de una API del clima. El resultado de la consulta es la misma para todos los usuarios y cambia solamente cada hora. Sin embargo, cada consulta tarda un tiempo promedio de 15 segundos. Esto implica que cada usuario deberá esperar esos 15 segundos para ver el resultado (sumado al tiempo que tome la visualización y otros procesos que deba real...

Sesión de usuarios en aplicaciones web

Uno de los módulos más importantes y a la vez menospreciados cuando se aborda la tarea de crear un sitio web de servicios, ya sea para una intranet corporativa o un sistema de gestión de información ( SGI ) es la gestión y administración  requerida para una correcta implementación de sesiones de usuario. Y es que llevamos tanto tiempo usando usuarios y contraseñas en Internet, en cualquiera de sus muchas variaciones, que se asume muchas veces que esto ya forma parte del ADN de toda solución web y como tal, se destina muy poco tiempo y estudio a este apartado cuando se planifican las actividades de desarrollo. Lo cierto es que cada aplicación acostumbra desarrollar su propio esquema de manejo de sesiones y asumir que es algo superfluo puede equivaler a “pegarse un tiro en el pie”, especialmente cuando un módulo de este tipo se diseña desde ceros. Al referirse al manejo de sesiones de usuario suele pensarse únicamente en el proceso de capturar el nombre de usuario ( username ) y su ...