Captura de errores en PHP

Facebooktwittergoogle_pluslinkedinmailFacebooktwittergoogle_pluslinkedinmail

Cuando desarrollamos un proyecto en PHP, sobre todo si es de cierta envergadura, es inevitable que, en las primeras fases (y, en ocasiones, no tan “primeras”) se produzcan errores. Existe un viejo adagio en programación que dice que “Un programa nunca funciona bien a la primera y, si no funciona a la primera, ya no funcionará bien nunca”. Esto es, quizá, un poco exagerado… pero sólo un poco.

Lo cierto es que, en nuestra aplicación PHP vamos a encontrar, básicamente, tres tipos de errores:

  • Los errores fatales. Son muy fáciles de detectar y capturar, ya que impiden la ejecución del programa y, si tenemos activada la directiva display_errors, nos dan un mensaje muy descriptivo en pantalla, incluyendo, por supuesto, el script y línea dónde se ha producido el error. Si esta directiva está desactivada, la respuesta suele ser un error de tipo 500, y el bloqueo de la carga de la página.
  • Los errores de lógica. Estos son, sin duda, los más difíciles de capturar ya que, en la mayoría de las ocasiones, el script se ejecuta completamente, aunque sus resultados no sean los esperados.
  • Los errores de tipo Warning o Notice. Muestran un mensaje en pantalla, pero no interrumpen la ejecución. Es el caso, por ejemplo, de querer emplear una variable no declarada. Si no tenemos activada la opción display_errors (y, en un servidor de producción no debe estar activada nunca, por razones de seguridad), es muy fácil confundirlos con errores de lógica, y dar por bueno un script que, en realidad, no está haciendo o que debe.

En este artículo aprenderemos un pequeño recurso de PHP que nos facilitará la tarea de capturar errores.

LA CAPTURA “TRADICIONAL”

Uno de los sistemas más empleados por los desarrolladores para la captura de errores es el uso de bloques try...catch. Esto es válido no solo en PHP sino en todos los lenguajes de alto nivel modernos. El principio es muy simple. Aquellos fragmentos de código que pueden dar lugar a algún tipo de problema son encerrados dentro de un bloque try, proporcionando una “salida honrosa” en caso de que se produzca una excepción. Quizá el ejemplo más clásico sea el de tratar de establecer una conexión a base de datos, como el siguiente:

Esta es la manera en que todos capturamos posibles excepciones en nuestro código. Es muy eficiente y, si nos lo “curramos” un poco, podemos dar una solución a casi todas las excepciones. El objeto Exception (en nuestro ejemplo $e) proporciona cierta información sobre la excepción ocurrida, que nos permite tomar la mejor solución en cada caso.

Este sistema presenta tres limitaciones:

  • No todos los errores lanzan excepciones. Los errores de tipo Warning o Notice, por ejemplo, no lo hacen.
  • Puede resultar un poco laborioso usar un bloque try...catch en cada parte de cada script de nuestro proyecto que puede lanzar excepciones. Por inercia, o por limitaciones de tiempo, cuando desarrollamos tendemos a usar esta técnica sólo en unos pocos puntos “clave” del proyecto.
  • A menos que lo programemos específicamente, este sistema no guarda un log de las excepciones producidas, lo que supone un trabajo adicional y, potencialmente, otra posible fuente de excepciones.

Lo anterior no significa, de ninguna manera, que debamos renunciar al uso de bloques try...catch. Al contrario. En la programación actual son… iba a decir que muy necesarios, pero creo que la palabra adecuada es imprescindibles. Sin embargo, debemos tener muy claro qué es esta estructura, para qué sirve y para qué usos no es lo más adecuado.

EL OBJETO Exception

Cómo decía anteriormente, el objeto Exception proporciona gran cantidad de información acerca del origen y la causa de la excepción que se ha producido. Es una matriz similar a la que ves a continuación:

  • La clave message devuelve el mensaje que, de no usar la captura de excepciones, habría sido volcado como Fatal Error.
  • La clave code devuelve el código numérico de la excepción.
  • La clave file devuelve el nombre del script en el que se ha producido la excepción.
  • La clave line devuelve la línea en la que se ha producido la excepción.
  • La clave trace devuelve, a su vez, otra matriz con datos como la clase que ha lanzado la excepción, el método dentro de esa clase donde se ha producido, y los argumentos que recibió dicho método.

Toda esta información es extremadamente útil, a la hora de depurar y mantener nuestro código.

GENERAR LOGS

Cómo decíamos antes, el bloque try...catch, a pesar de todo, presenta algunas limitaciones. Necesitamos un sistema de rastreo de errores que genere un log que podamos consultar, en todo el proyecto, sea cual sea el punto donde se produzca un error, y sea cual sea el nivel del error producido. Este mecanismo, en PHP, nos lo proporciona la función set_error_handler(). Esta función recibe, como argumento, el nombre de una función de callback (que deberá estar previamente declarada) y, una vez activado en un script, registrará cada error, notificación o aviso que se produzca. La función de callback será la encargada de generar el correspondiente log, y almacenarlo o enviarlo a donde proceda. El nombre de la función de callback se pasará como una cadena. La sistaxis general, por lo tanto, queda así:

set_error_handler("LogDeErrores");

Esta función puede recibir un segundo argumento opcional, si queremos limitar los niveles de errores que deben ser gestionados por este mecanismo. Cómo ya sabes, los niveles de errores en PHP se identifican mediante unas constantes propias del lenguaje, que ya están predefinidas. Los niveles de error que puede manejar set_error_handler() son los siguientes: E_WARNING, E_NOTICE, E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE, E_RECOVERABLE_ERROR, E_DEPRECATED, E_USER_DEPRECATED. Así, si queremos gestionar por este procedimiento los errores de tipo Notice y Warning solamente, la sintaxis de la función quedaría así:

set_error_handler("LogDeErrores", E_WARNING|E_NOTICE);

Mi opinión es que, a menos que resulte imprescindible filtrar, no me gusta usar el segundo argumento.

Lo suyo es incorporar este mecanismo al principio de un script que vaya a ser incluido en todo el proyecto. El ejemplo más habitual es un script que contiene la definición del acceso a base de datos. Dicho script, cómo es lógico, se incluirá en cualquier otro script del proyecto, por lo que, al igual que la conexión a base de datos, el mecanismo de log de errores estará activo en cada script.

LA FUNCIÓN DE CALLBACK

La función de callback referenciada en set_error_handler() recibe hasta cinco argumentos, de los que sólo los dos primeros son obligatorios, que le aporta el propio intérprete de PHP cuando se produce un error. Los argumentos son cinco variables (que podemos llamar como deseemos):

$numError Contiene un código numérico que referencia el tipo de error.

$descripcion Contiene el literal que describe el tipo de error producido (p.e. Undefined variable: nombre si hemos tratado de usar una variable llamada $nombre que no ha sido declarada). Realmente, esta descripción en el log es más útil que el parámetro anterior.

$fichero Contiene el nombre del script que ha producido el error.

$linea Contiene el número de línea donde se ha producido el error.

$contexto Contiene las matrices superglobales $_GET, $_POST y $_FILES. Además, si nuestro script hace uso de cookies o sesiones, también estarán las matrices $_COOKIE y/o $_SESSION.

EL GENERADOR DE LOGS

Con lo que hemos visto hasta ahora, nuestro generador de logs de errores empieza a tomar forma. Aún no es operativo, pero empieza a esbozarse. Actualmente, tenemos ya lo siguiente:

La función LogDeErrores() se dispara cada vez que se produce un error. Los cinco argumentos son recibidos en el interior del cuerpo de la función. Ahora veamos que hacer con ellos. Podemos almacenarlos en una base de datos, o en un fichero de texto, o enviarlos a un correo electrónico donde los recibiremos.

LA FUNCIÓN error_log()

Esta función nos permite almacenar los errores en un fichero de texto, o enviarlos a un correo electrónico, que suelen ser las acciones más habituales. Necesita, para funcionar, los siguientes argumentos:

  • $mensaje Es el único parámetro obligatorio. Contiene una cadena de texto que puede estar formada por algunos de los argumentos recibidos en la función de callback (o todos ellos).
  • $acción Es un valor numérico que indica que se hará con el mensaje. Es opcional, y el valor por defecto es 0. Los posibles valores son:
    • 0 El log se guardará en el registro del sistema de PHP, usando el mecanismo de registro del sistema operativo o un fichero.
    • 1 El log será enviado a un correo electrónico.
    • 2 Este valor está deprecado y ya no se emplea.
    • 3 El log es almacenado en un fichero que le indiquemos a la función.
    • 4 El log es enviado directamente al gestor de registro de la SAPI (para más información sobre la SAPI visita este enlace).
  • $destino Es un parámetro opcional. Si usamos, en el parámetro anterior, el valor 1, aquí especificaremos el correo de destino. En caso de usar 3, aquí especificaremos el nombre (y la ruta, en su caso) del archivo donde se almacenará el mensaje del error.
  • $cabeceras Es opcional. Se usa para incluir cabeceras adicionales en el caso de que el log se envíe a un correo electrónico. Para saber más sobre estas cabeceras visita este enlace.

Para almacenar los logs de errores en un archivo de texto plano, la sintaxis de error_log() podría parecerse a la siguiente:

error_log("Error: [".$numeroDeError."] ".$descripcion." ".$fichero." ".$linea." ".json_encode($contexto)." \n\r", 3, "log_errores.txt");

Si la analizas, verás que el primer argumento es una cadena formada por los argumentos que recibimos en la función de callback.

Si queremos enviar el log a un correo electrónico, la sintaxis de la función podría parecerse a la que ves a continuación:

error_log("Error: [".$numeroDeError."] ".$descripcion." ".$fichero." ".$linea." ".json_encode($contexto)." \n\r", 1, "destino@miservidor.com", "From: admin@proyecto.com");

Observa que el argumento $contexto es una matriz de matrices superglobales. Como el primer argumento de error_log() es una cadena, convertimos la matriz a cadena en formato JSON. Si pusiéramos la matriz directamente, la función daría un error. Y, desde luego, si nuestro mecanismo de captura de errores produce errores, mal vamos.  😛 .

MONTANDO TODO EL MECANISMO

Vale. Ya hemos hablado de todas las piezas que forma el mecanismo de log de errores. Ahora veamos como montarlo. La función error_log() debe ir, a su vez, en el cuerpo de la función de callback. El conjunto, para grabar los logs de errores en un archivo de texto, podría quedar así:

Si quisiéramos que el log se enviara a un correo electrónico en lugar de a un archivo de texto, el mecanismo puede tener el siguiente aspecto:

Esto enviará cada log a la dirección mi_correo@mi_servidor.com y, como remitente, aparecerá proyecto@proyecto.com.

GRABAR EL LOG EN UNA BASE DE DATOS

Es posible grabar el log en una base de datos. En este caso, ni siquiera necesitaremos la función error_log(). Lo que si nos hará falta será recuperar, en la función de callback, la conexión a la base de datos a utilizar, por supuesto. Supongamos que, en nuestra base de datos, tenemos una tabla llamada registro_de_logs, destinada, precisamente, a este efecto. Nuestro mecanismo de captura podría ser algo parecido a lo siguiente:

Y ya lo tenemos. Todos los logs de errores en una tabla, para poder analizarlos en cada caso.

     

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *