Excepciones en PHP
Este articulo pretende ser la segunda parte de éste otro, así pues, continuemos 😉
Como comenté en el artículo anterior, los errores y las excepciones, en cualquier lenguaje de programación, no son exactamente lo mismo. A veces, sobre todo al principio, podemos tratar una excepción como un error tal cual, y no es del todo así. No nos referimos a lo mismo.
Podemos decir que una excepción es un problema excepcional y previsible y que, además, podemos controlar.
Imagínate que un usuario introduce un email no válido, es decir, no tiene el formato correcto de email.
Por ejemplo: unemailcualquiera@un.dominio@cualquiera
En ese caso, se estaría produciendo una excepción. El email no existe y nosotros no aceptamos ese formato, por tanto, es una situación excepcional y previsible que podemos controlar.
Otro ejemplo puede ser el acceso no autorizado. Cuando un usuario intenta acceder a una parte del sistema sin los permisos necesarios, se puede generar una excepción.
Manejando Excepciones en PHP
Para trabajar con excepciones debemos de hacer uso del bloque try-catch:
try: Dentro de este bloque, añadimos el código que puede lanzar una excepción.
catch: Aquí se captura la excepción lanzada en el bloque try y es aquí, donde manejamos el error, es decir, podemos registrar el error, mostrar un mensaje “más amigable” al usuario, etc.
La estructura básica sería la siguiente:
try { // Aquí el código que puede producir un excepcion } catch (Exception $e) { // Aquí manejamos la excepción }
Un ejemplo real puede ser el dividir un numero entre cero:
try { // Una división entre cero generará una excepción $resultado = 10 / 0; } catch (DivisionByZeroError $e) { // Manejo de la excepción echo "Error: " . $e->getMessage(); }
El resultado sería: Error: Division by zero
En el ejemplo anterior, estamos realizando una división por cero, lo cual nos lanza una excepción de tipo DivisionByZeroError. Gracias al bloque catch capturamos esta excepción y manejamos el error mostrando un mensaje… “más amigable” en lugar de que se interrumpa el programa bruscamente.
Cuando una excepción es lanzada, PHP busca el bloque catch correspondiente para manejarla. Si no hay un bloque catch, el programa generará un error fatal.
Por ejemplo, ¿Qué pasaría si escribiéramos el código de la siguiente manera sin capturar la excepción?
$resultado = 10 / 0; echo $resultado;
Como he dicho antes, obtendríamos el siguiente error fatal:
Fatal error: Uncaught DivisionByZeroError: Division by zero in C:\laragon\www\PHP\temas blog\ejemplos\errores-y-excepciones\excepciones.php:3 Stack trace: #0 {main} thrown in C:\laragon\www\PHP\temas blog\ejemplos\errores-y-excepciones\excepciones.php on line 3
¿Ves la diferencia?
Recordemos, una excepción representa un error, sí, pero digamos que nos permite preverlo y en caso de que se produzca, controlarlo y manejarlo como queramos.
Uso de throw
No solo disponemos del bloque try-catch para las excepciones, sino que PHP dispone de la palabra clave throw que nos permite lanzar manualmente una excepción.
throw: Permite lanzar una excepción cuando se detecta un error que quieres manejar explícitamente.
Siguiendo con el ejemplo de la división por cero:
// Uso de throw function dividir($dividendo, $divisor) { if ($divisor == 0) { throw new Exception("Imposible dividir por cero."); } return $dividendo / $divisor; } try { echo dividir(10, 0); } catch (Exception $e) { echo "Error: " . $e->getMessage(); }
El resultado sería: Error: Imposible dividir por cero.
Como vemos, en la función dividir, lanzamos la excepción manualmente con throw new Exception(). Si se produce esta excepción (el divisor es igual a cero), se lanza esta excepción personalizada y se captura en el bloque catch, donde la manejamos mostrando (una vez más) un mensaje más amigable, aunque podemos hacer lo que queramos, claro está.
Jerarquía de excepciones
Como hemos visto, al usar throw, también hemos usado new Exception(). Pues bien, esto es porque todas las excepciones en PHP heredan de la clase base Exception. Esto, como veremos más adelante nos permite crear nuestras propias excepciones heredando de dicha clase.
Por otro lado, decir también que PHP dispone de excepciones ya predefinidas como ErrorException, RuntimeException, InvalidArgumentException, entre otras.
Por ejemplo, la excepción InvalidArgumentException, sirve para indicar que un argumento pasado a una función o método no es válido.
El siguiente ejemplo sirve para comprobar si un número dado es positivo; si no lo es, lanza una excepción de tipo InvalidArgumentException:
function validarNumeroPositivo($numero) { if ($numero <= 0) { throw new InvalidArgumentException("El número debe ser mayor que cero: $numero"); } return "Número válido: $numero"; } try { echo validarNumeroPositivo(-5); } catch (InvalidArgumentException $e) { echo "Excepción capturada: " . $e->getMessage(); }
Clase base Exception
Como hemos visto en todos los ejemplos puestos hasta ahora, la clase base es la clase Exception, de la cual heredan todas las excepciones, incluidas las predefinidas.
La clase Exception tiene varias propiedades y métodos que resultan útiles para obtener detalles sobre el error producido.
Por ahora, en todos los ejemplos anteriores solo hemos visto el método getMessage(), la cual nos devolvía el mensaje del error, sin embargo, hay otras como:
getCode(): Devuelve el código de error de la excepción. Puede ser útil para categorizar tipos de errores.
getFile(): Devuelve el nombre del archivo donde se originó la excepción.
getLine(): Devuelve la línea donde ocurrió el error.
getPrevious(): Devuelve la excepción lanzada anteriormente. Es decir, si la excepción fue lanzada en cadena, esta propiedad contendrá la excepción anterior. Esto lo explicaré más adelante, tranquil@ 😊
getTrace(): Devuelve un array que contiene la traza completa del stack en el momento de lanzar la excepción. En otras palabras, nos muestra qué funciones fueron llamadas hasta llegar a la excepción.
Excepciones en cadena
Se trata de capturar una excepción inicial, y al mismo tiempo lanzar una nueva excepción que incluya a la original.
Esto es útil para agregar más información adicional sin perder el rastro del error original.
Recuerdas que la clase base Exception() tenía un método getPrevious(), pues bien, este método nos sirve para hacer esto que queremos, lanzar excepciones en cadena y poder rastrear el origen del problema y también ver información mucho más detallada del error.
Si te parece confuso tranquil@, con un ejemplo en código se ve mejor:
function dividir($dividendo, $divisor) { if ($divisor == 0) { // Lanza una excepción si el divisor es cero throw new Exception("División por cero"); } return $dividendo / $divisor; } function realizarDivision($a, $b) { try { return dividir($a, $b); } catch (Exception $e) { // Encadena la excepción original dentro de una nueva throw new Exception("Error al realizar la división", 0, $e); } } try { // Intenta dividir por cero echo realizarDivision(10, 0); } catch (Exception $e) { // Captura la excepción encadenada y muestra la información echo "Excepción capturada: " . $e->getMessage() . PHP_EOL; echo "Causa original: " . $e->getPrevious()->getMessage(); }
Lo que hemos hecho ha sido:
1º La función dividir lanzará una excepción si el divisor es cero.
2º La función realizarDivision llama a dividir, y si se lanza una excepción, captura esa excepción y lanza una nueva excepción en cadena incluyendo la original.
3º En el bloque try-catch se muestra tanto el mensaje de la excepción encadenada como el mensaje de la excepción original.
El resultado:
Excepción capturada: Error al realizar la división
Causa original: División por cero
Como se puede observar nos da un poco más de información detallada sobre el error.
Excepciones múltiples
PHP nos permite añadir también, en un mismo bloque catch, diferentes tipos de excepciones, permitiendo así que se lance una u otra según el caso.
Para ello debemos de usar el operador |.
Esto reduce el código evitando tener varios bloques catch para cada tipo de excepción.
function validarDato($dato) { if (!is_numeric($dato)) { // InvalidArgumentException si el dato no es numérico throw new InvalidArgumentException("El dato debe ser numérico."); } if ($dato <= 0) { // RangeException si el número es menor o igual a 0 throw new RangeException("El número debe ser mayor que cero."); } return "Dato válido: $dato"; } try { echo validarDato("texto"); } catch (InvalidArgumentException | RangeException $e) { // Captura ambas excepciones y muestra el mensaje echo "Excepción capturada: " . $e->getMessage(); }
El ejemplo anterior es bastante simple y descriptivo. Dependiendo de la validación del número, lanzará una InvalidArgumentException o una RangeException.
1º La función validarDato comprueba si el dato es numérico y mayor que cero.
2º Si el dato no es numérico, lanza una excepción de tipo InvalidArgumentException.
3º Si es numérico pero menor o igual a 0, lanza una excepción de tipo RangeException.
4º Finalmente en el bloque try-catch capturamos ambas excepciones, mostrando el mensaje de error correspondiente.
Uso de finally
Por último y para ir acabando sobre el tema, decir también que PHP cuenta con un bloque llamado finally, el cual se ejecuta siempre, sin importar si ocurrió una excepción o no. Es útil para liberar recursos o realizar cualquier acción que deba de ejecutarse siempre.
Un ejemplo de esto puede ser cerrar la conexión a una base de datos, cerrar un archivo o… simplemente mostrar un mensaje.
Siguiendo con el ejemplo de la división por cero:
function dividir($dividendo, $divisor) { if ($divisor == 0) { throw new Exception("División por cero"); } return $dividendo / $divisor; } try { echo dividir(10, 0); } catch (Exception $e) { echo "Excepción capturada: " . $e->getMessage() . PHP_EOL; } finally { // Este bloque se ejecutará siempre echo "Operación finalizada." . PHP_EOL; }
1º La función dividir lanzará una excepción si el divisor es cero.
2º En el bloque try, dividimos 10 entre 0, lo cual lanzará una excepción.
3º La excepción es capturada en el bloque catch, y muestra el mensaje de error.
4º El bloque finally se ejecuta siempre, independientemente de que haya ocurrido una excepción o no, así que se muestra el mensaje «Operación finalizada.». Si cambiamos por un numero diferente a cero, es decir, que el divisor sea otro número, el bloque finally se ejecutará igualmente.
Y creo que ahora sí, esto es todo por ahora sobre las excepciones en PHP.
Hemos visto que las excepciones nos permiten gestionar los problemas en nuestra aplicación de manera más controlada y detallada. Una vez más conseguimos un código más limpio, legible, organizado y un manejo flexible de los errores. Si, todo son ventajas, pero hay que también saber cuando usarlas y cuando no.