Manejo de errores en PHP 7. Gestionando errores


Manejo de errores en PHP 7. Gestionando errores

Reunimos lo que hemos visto en los artículos anteriores en una sola clase y así configurar un controlador global de errores.



Controlar los errores que se producen durante el desarrollo de una aplicación web es fundamental y mas aun durante la fase de producción. La idea de este control es básicamente evitar que estos errores aparezcan en tiempos de ejecución dejando expuestas vulnerabilidades de nuestro código.
Ya hablamos sobre los errores y las excepciones en artículos anteriores y que estas clases implementan una interface Throwable lo que los hace mucho mas sencillo de atrapar y arreglar, sin embargo hemos dicho que aun muchos errores se convierten en errores fatales y es a estos últimos a los que nuestro código debe atender con mayor énfasis.
Vimos que el proceso de propagación de un error (o excepción) cualquiera es:

  • Ser atrapado en un bloque catch() que lo soporte
  • Ser manejado por funciones como set_error_handler(), set_esception_handler() o register_shutdown_function()
  • O en ultimo caso, generar un feo error fatal

Pasos previos

En un archivo index.php usaremos como código base:
/* index.php */
declare(strict_types=1);
ini_set('error_reporting', '-1');
ini_set('display_errors', '1');
define("DS", DIRECTORY_SEPARATOR);
define("RUTA", realpath(dirname(__FILE__)) . DS);
define("RUTA_CORE", RUTA . "core" . DS);
require_once(RUTA_CORE . 'Autoload.php');
$cargardor = new \Autoload\Autoload;
$cargardor->register();
$cargardor->addNamespace('Blockpc\\Clases', RUTA . 'clases');
$cargardor->addNamespace('Blockpc\\Errores', RUTA_CORE . 'errores');
use Blockpc\Errores\ControlErrores;
use Blockpc\Clases\Usuario;
try {
$usuario = new Usuario;
/* Acá va nuestro código */
} catch(Throwable $e) {
echo '<pre><b>Error atrapado por el bloque catch()</b></br>';
echo '<b>Clase</b>: '; print_r(get_class($e)); echo '</br>';
echo '<b>Linea</b>: '; print_r($e->getLine()); echo '</br>';
echo '<b>Archivo</b>: '; print_r($e->getFile()); echo '</br>';
echo '<b>Mensaje</b>: '; print_r($e->getMessage()); echo '</pre>';
exit;
}
Es el mismo código que hemos venido usando en artículos anteriores y en el que declaramos algunas configuraciones, definimos algunas constantes, cargamos un autoload y tenemos un bloque try...catch() para realizar nuestros ejemplos.
Cargamos una clase Usuario que es bien simple. con un constructor y un destructor para tener una idea como va corriendo nuestro código:
/* Clase Usuario */
namespace Blockpc\Clases;
class Usuario {
public function __construct() {
echo "Usuario constructor...<br>";
}
public function __destruct() {
echo "... Usuario destructor.<br>";
}
}
Así, como en este momento no tenemos errores deberíamos leer en el navegador las frases Usuario constructor... y ... Usuario destructor. demostrando que el archivo index.php fue leído y ejecutado por PHP completamente.
Por otro lado, estamos atrapando en el bloque catch() todos los errores o excepciones que sean objetos throwables y con esto cumplimos el primer paso del proceso propagación de un error o excepción.

Nuestro controlador de errores

Un error fatal lo podríamos definir como un error no capturado durante el proceso mencionado al inicio del articulo y que llego a la fase tres de este, detendrán la aplicación en el script en el cual se generen y podrían dejar al descubierto debilidades de nuestro sistema.
Para tratar estos errores creamos una clase ControlErrores que se encargara de detectarlos por medio de un registro con las funciones set_error_handler(), set_esception_handler() y que también incluirá la función register_shutdown_function() para el manejo de errores fatales.
/* Clase ControlErrores */
namespace Blockpc\Errores;
class ControlErrores {
private $_registrado = false;
private $_ultimoError = '';
public function registrar() {
set_error_handler(array($this, 'manejadorErrores'));
$this->_registrado = true;
}
public function restaurar() {
$this->_registrado = false;
restore_error_handler();
}
public function manejadorErrores($errno, $errstr, $errfile, $errline) {
$e = new \ErrorException($errstr, $errno, 0, $errfile, $errline);
try {
$this->errorInterno($e);
} catch (\Throwable $e2) {
$this->manejarInterno($e2);
}
}
private function errorInterno(\Throwable $e) {
$this->_ultimoError = $e;
$this->logError("Error desde el manejador de errores!");
}
private function manejarInterno(\Throwable $e) {
$this->_ultimoError = $e;
$this->logError("Error desconocido!");
}
private function logError(string $titulo) {
echo '<pre><b>' . $titulo . '</b></br>';
echo '<b>Clase</b>: '; print_r(get_class($this->_ultimoError)); echo '</br>';
echo '<b>Linea</b>: '; print_r($this->_ultimoError->getLine()); echo '</br>';
echo '<b>Archivo</b>: '; print_r($this->_ultimoError->getFile()); echo '</br>';
echo '<b>Mensaje</b>: '; print_r($this->_ultimoError->getMessage()); echo '</pre>';
exit();
}
}
Revisemos la utilidad de las funciones de nuestra clase.
registrar() Registramos el método manejadorErrores() para gestionar los errores.
restaurar() Restauramos la gestión de errores al que maneja PHP por defecto.
manejadorErrores() Nuestro método para gestionar cualquier error que no sea capturado por el bloque catch(). Este método transforma el error atrapado a un objeto ErrorException para que pueda ser informado por logError. Si no es posible transformar el error, actuara el método manejarInterno y sera informado.
errorInterno() Asigna un error como ultimo error.
manejarInterno() Asigna un error como ultimo error de forma interna a la clase, pues se entiende que el método errorInterno() a producido otro error.
logError() Genera un log de salida con la información del ultimo error generado.
En esta clase solo les muestro el código referente al manejo de errores, sin embargo el código final que estará para descargar al terminar el articulo, complementa el manejo de excepciones (Que si bien no es necesario por que de hecho, estas son atrapadas por el bloque catch()).

Creando Errores

Crearemos un conjunto de errores y para procesarlos creamos un objeto de nuestra clase ControlErrores y llamamos a su método registrar() antes de todo nuestro código funcional. Ademas debemos llamar al método restaurar() al final de todo, en nuestro caso antes del exit() del bloque catch().
/* Primeros errores */
try {
$controlError = new ControlErrores();
$controlError->registrar();
$usuario = new Usuario;
/* Acá irán nuestros errores */
} catch(Throwable $e) {
echo '<pre><b>Error atrapado por el bloque catch()</b></br>';
echo '<b>Clase</b>: '; print_r(get_class($e)); echo '</br>';
echo '<b>Linea</b>: '; print_r($e->getLine()); echo '</br>';
echo '<b>Archivo</b>: '; print_r($e->getFile()); echo '</br>';
echo '<b>Mensaje</b>: '; print_r($e->getMessage()); echo '</pre>';
$controlError->restaurar();
exit;
}
El primer error que lanzaremos sera uno generado por la función trigger_error() de PHP. Esta función genera un error emitido por nosotros, el usuario desarrollador, y que trabaja en conjunto con set_error_handler().
Recibe dos parámetros, un string como mensaje de error y un tipo de error de la familia de constantes E_USER, y por defecto es E_USER_NOTICE.
/* Error emitido por trigger_error() */
trigger_error("Un primer error generado para pruebas", E_USER_ERROR);
Como este error no es atrapado por el bloque catch(), es atrapado por nuestra función manejadorErrores() de la clase que creamos y mostrara por web un mensaje muy similar a este.
/* Salida */
Usuario constructor...
Error desde el manejador de errores!
Clase: ErrorException
Linea: 27
Archivo: ...\articulo28\index.php
Mensaje: Un primer error generado para pruebas
... Usuario destructor.
Un segundo error lo lanzaremos a través de la función de PHP strpos() a la cual no le pasaremos parámetros.
 /* Error ArgumentCountError --> Lo atrapa el catch() */
strpos(); /* ArgumentCountError --> Lo atrapa el catch() */
Este error es atrapado por el bloque catch() y tendrá una salida como la anterior pero generada dentro del bloque.
Un tercer error, que no es atrapado por el bloque catch(), son las excepciones aritméticas.
/* Error Division by zero --> Lo atrapamos desde manejadorErrores() */
$dividendo = 10;
$divisor = 0;
/* if ($divisor == 0) {
trigger_error("No se puede dividir por cero", E_USER_ERROR);
} */
$cociente = $dividendo / $divisor;
Así como esta este código, el error generado sera atrapado por la función manejadorErrores(). Incluso si descomentas ese condicional if(). Por otro lado si comentas, lo relacionado a la clase ControlErrores y el objeto creado notaras que el error no se puede atrapar en el bloque catch() y habría que recurrir a una clase especial, como la creada en el articulo sobre excepciones.
Un ultimo error que no es atrapado por un bloque catch() y que se comporta como un error fatal es usando require
/* Error Fatal --> Imposible de atrapar, de no ser por manejadorErrores() */
require "archivo-que-no-existe.php":
Esta estructura de PHP genera un error fatal de nivel E_COMPILE_ERROR solo atrapable con la función manejadorErrores() generando una salida mucho mas amena y manejable.
/* Salida */
Usuario constructor...
Error desde el manejador de errores!
Clase: ErrorException
Linea: 35
Archivo: ...\articulo28\index.php
Mensaje: require(archivo-que-no-existe.php.php): failed to open stream: No such file or directory
... Usuario destructor.
El script no se detiene, continua su ejecución. Sin la función manejadorErrores se detendría y se generaría un feo mensaje, tal vez mostrando mas información de la necesaria. Ahora, podemos manejar ese error, quizás podríamos generar un mensaje en pantalla y ademas enviárnoslo al correo.

Otras funciones

La clase ControlErrores ademas tiene implementado un par de métodos mas que ayudan a atrapar algunos otros errores que podrían escaparse.
/* Otros Métodos */
public function registrar() {
/* Agregamos este nuevo registro */
register_shutdown_function(array($this, 'manejadorErroresNoAtrapados'));
}
private function hashError(\ErrorException $e) {
return md5($e->getCode() . ';' . $e->getMessage() . ';' . $e->getFile() . ';' . $e->getLine());
}
public function manejadorErroresNoAtrapados() {
if (!$this->_registrado) {
return false;
}
$error = error_get_last();
if ($error) {
$e = new \ErrorException($error['message'], $error['type'], 0, $error['file'], $error['line']);
if ($this->_ultimoError !== null && $this->hashError($e) == $this->hashError($this->_ultimoError)) {
return false;
}
try {
$this->_ultimoError = $e;
$this->logError("Error desde el manejador de errores fatales!");
} catch(\Throwable $e2) {
$this->manejarInterno($e2);
}
}
}
El método manejadorErroresNoAtrapados() que se registra bajo register_shutdown_function(), se ejecuta al terminar un script, en nuestro caso seria el archivo index.php en la linea del exit().
Y valida, a través de un hash md5(), que si aun queda algún error que no fue procesado lo haga. Nos permite asegurarnos que los errores se vayan mostrando uno por uno y no todos de golpe jeje.
Puedes comentar el exit() del método logError() de la clase ControlErrores y veras que ya no sale solo un error.

Ultimas consideraciones

Como siempre, este código se puede ir mejorando mas, pero dependerá de tu configuración de PHP como por ejemplo en el tiempo máximo de ejecución de un script, que es otro error fatal típico.
Por otro lado, incluso en este código podrías comentar el bloque try... catch() y veras que la clase funciona igual.
/* Comentando el try...catch() */
//try {
$controlError = new ControlErrores();
$controlError->registrar();
$usuario = new Usuario;
trigger_error("Un primer error generado para pruebas", E_USER_ERROR);
strpos(); /* ArgumentCountError --> Lo atrapa el catch() */
$dividendo = 10;
$divisor = 0;
/* if ($divisor == 0) {
trigger_error("No se puede dividir por cero", E_USER_ERROR);
} */
$cociente = $dividendo / $divisor; /* Division by zero --> Lo atrapamos desde manejadorErrores() */
require "archivo-que-no-existe.php"; /* Error Fatal --> Imposible de atrapar, de no ser por manejadorErrores() */
// } catch(Throwable $e) {
// echo '<pre><b>Error atrapado por el bloque catch()</b></br>';
// echo '<b>Clase</b>: '; print_r(get_class($e)); echo '</br>';
// echo '<b>Linea</b>: '; print_r($e->getLine()); echo '</br>';
// echo '<b>Archivo</b>: '; print_r($e->getFile()); echo '</br>';
// echo '<b>Mensaje</b>: '; print_r($e->getMessage()); echo '</pre>';
$controlError->restaurar();
exit();
// }

Enlace al articulo sobre errores.
Enlace al articulo sobre excepciones.
Enlace a register_shutdown_function()
Enlace a set_error_handler()
Enlace a register_shutdown_function()

El código fuente lo puedes descargar desde acá

Bueno amigo, los dejo y hasta el próximo articulo!

Etiquetas php errores

Ultima actualización Lunes 25 de Junio, 2018




Agregar Comentario