Spanish (Español) translation by Luis Chiabrera (you can also view the original English article)
En este artículo vamos a aprender acerca de las excepciones de PHP desde cero. Estos conceptos se utilizan en muchas aplicaciones y marcos grandes, escalables y orientados a objetos. Aproveche esta función de lenguaje para mejorar sus habilidades como desarrollador de aplicaciones web.
1 un ejemplo primero
Antes de comenzar con todas las explicaciones, me gustaría mostrar un ejemplo primero.
Digamos que quieres calcular el área de un círculo, por el radio dado. Esta función hará eso:
1 |
|
2 |
function circle_area($radius) { |
3 |
|
4 |
return pi() * $radius * $radius; |
5 |
|
6 |
}
|
Es muy simple, sin embargo, no comprueba si el radio es un número válido. Ahora vamos a hacer eso, y lanzaremos una excepción si el radio es un número negativo:
1 |
|
2 |
function circle_area($radius) { |
3 |
|
4 |
// radius can't be negative
|
5 |
if ($radius < 0) { |
6 |
throw new Exception('Invalid Radius: ' . $radius); |
7 |
} else { |
8 |
return pi() * $radius * $radius; |
9 |
}
|
10 |
|
11 |
}
|
Veamos qué pasa cuando lo llamamos con un número negativo:
1 |
|
2 |
$radius = -2; |
3 |
|
4 |
echo "Circle Radius: $radius => Circle Area: ". |
5 |
circle_area($radius) . "\n"; |
6 |
|
7 |
|
8 |
echo "Another line"; |
El script se bloquea con el siguiente mensaje:
1 |
|
2 |
<br /> |
3 |
<b>Fatal error</b>: Uncaught exception 'Exception' with message 'Invalid Radius: -2' in C:\wamp\www\test\test.php:19 |
4 |
Stack trace: |
5 |
#0 C:\wamp\www\test\test.php(7): circle_area(-2) |
6 |
#1 {main}
|
7 |
thrown in <b>C:\wamp\www\test\test.php</b> on line <b>19</b><br /> |
Ya que fue un error fatal, no ocurrió más ejecución de código después de eso. Sin embargo, es posible que no siempre desee que los scripts se detengan cada vez que se produce una excepción. Afortunadamente, puedes "atraparlos" y manejarlos.
Esta vez, vamos a hacer una serie de valores de radio:
1 |
|
2 |
$radius_array = array(2,-2,5,-3); |
3 |
|
4 |
foreach ($radius_array as $radius) { |
5 |
|
6 |
try { |
7 |
echo "Circle Radius: $radius => Circle Area: ". |
8 |
circle_area($radius) . "\n"; |
9 |
} catch (Exception $e) { |
10 |
echo 'Caught Exception: ', $e->getMessage(), "\n"; |
11 |
}
|
12 |
|
13 |
}
|
Ahora obtenemos esta salida:
1 |
|
2 |
Circle Radius: 2 => Circle Area: 12.566370614359 |
3 |
Caught Exception: Invalid Radius: -2 |
4 |
Circle Radius: 5 => Circle Area: 78.539816339745 |
5 |
Caught Exception: Invalid Radius: -3 |
No hay más errores y el script continúa ejecutándose. Así es como atrapas las excepciones.
Screencast completo
2 ¿Qué es una excepción?
Las excepciones han existido en otros lenguajes de programación orientados a objetos desde hace bastante tiempo. Fue adoptado por primera vez en PHP con la versión 5.
Por definición, una Excepción es 'lanzada', cuando ocurre un evento excepcional. Esto podría ser tan simple como una 'división por cero', o cualquier otro tipo de situación no válida.
1 |
|
2 |
throw new Exception('Some error message.'); |
Esto puede sonar similar a otros errores básicos que ya ha visto muchas veces. Pero las excepciones tienen un tipo diferente de mecanismo.
Las excepciones son en realidad objetos y usted tiene la opción de "atraparlos" y ejecutar cierto código. Esto se hace mediante el uso de bloques 'try-catch':
1 |
|
2 |
try { |
3 |
|
4 |
// some code goes here
|
5 |
// which might throw an exception
|
6 |
|
7 |
} catch (Exception $e) { |
8 |
|
9 |
// the code here only gets executed
|
10 |
// if an exception happened in the try block above
|
11 |
|
12 |
}
|
Podemos incluir cualquier código dentro de un bloque 'try'. El siguiente bloque 'catch' se usa para capturar cualquier excepción que pueda haber sido lanzada desde dentro del bloque try. El bloque catch nunca se ejecuta si no hubo excepciones. Además, una vez que ocurre una excepción, la secuencia de comandos salta inmediatamente al bloque catch, sin ejecutar ningún otro código.
Más adelante en el artículo, tendremos más ejemplos que deberían demostrar el poder y la flexibilidad de usar excepciones en lugar de simples mensajes de error.
3 Excepciones Bubble Up
Cuando se lanza una excepción desde una función o un método de clase, se dirige a quienquiera que haya llamado a esa función o método. Y sigue haciendo esto hasta que alcanza la parte superior de la pila O queda atrapado. Si llega a la parte superior de la pila y nunca se llama, obtendrá un error fatal.
Por ejemplo, aquí tenemos una función que lanza una excepción. Llamamos a esa función desde una segunda función. Y finalmente llamamos a la segunda función desde el código principal, para demostrar este efecto burbujeante:
1 |
|
2 |
function bar() { |
3 |
|
4 |
throw new Exception('Message from bar().'); |
5 |
|
6 |
}
|
7 |
|
8 |
function foo() { |
9 |
|
10 |
bar(); |
11 |
|
12 |
}
|
13 |
|
14 |
|
15 |
try { |
16 |
|
17 |
foo(); |
18 |
|
19 |
} catch (Exception $e) { |
20 |
echo 'Caught exception: ', $e->getMessage(), "\n"; |
21 |
}
|
Entonces, cuando llamamos a foo (), intentamos detectar cualquier excepción posible. A pesar de que foo () no lanza uno, pero bar () sí, todavía burbujea y queda atrapado en la parte superior, así que obtenemos una salida que dice: "Excepción detectada: Mensaje de bar ()".
4 excepciones de rastreo
Dado que las excepciones hacen burbujas, pueden venir de cualquier parte. Para facilitar nuestro trabajo, la clase de excepción tiene métodos que nos permiten rastrear el origen de cada excepción.
Veamos un ejemplo que involucra múltiples archivos y múltiples clases.
Primero, tenemos una clase de usuario y la guardamos como user.php:
1 |
|
2 |
class User { |
3 |
|
4 |
public $name; |
5 |
public $email; |
6 |
|
7 |
public function save() { |
8 |
|
9 |
$v = new Validator(); |
10 |
|
11 |
$v->validate_email($this->email); |
12 |
|
13 |
// ... save
|
14 |
echo "User saved."; |
15 |
|
16 |
return true; |
17 |
}
|
18 |
|
19 |
}
|
Utiliza otra clase llamada Validator, que colocamos en validator.php:
1 |
|
2 |
class Validator { |
3 |
|
4 |
public function validate_email($email) { |
5 |
|
6 |
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { |
7 |
throw new Exception('Email is invalid'); |
8 |
}
|
9 |
|
10 |
}
|
11 |
|
12 |
}
|
Desde nuestro código principal, vamos a crear un nuevo objeto de usuario, estableceremos el nombre y los valores de correo electrónico. Una vez que llamemos al método save (), utilizará la clase Validator para verificar el formato del correo electrónico, que podría devolver una excepción:
1 |
|
2 |
include('user.php'); |
3 |
include('validator.php'); |
4 |
|
5 |
|
6 |
$u = new User(); |
7 |
$u->name = 'foo'; |
8 |
$u->email = '$!%#$%#*'; |
9 |
|
10 |
$u->save(); |
Sin embargo, nos gustaría detectar la excepción, por lo que no hay ningún mensaje de error fatal. Y esta vez vamos a examinar la información detallada sobre esta excepción:
1 |
|
2 |
include('user.php'); |
3 |
include('validator.php'); |
4 |
|
5 |
try { |
6 |
|
7 |
$u = new User(); |
8 |
$u->name = 'foo'; |
9 |
$u->email = '$!%#$%#*'; |
10 |
|
11 |
$u->save(); |
12 |
|
13 |
} catch (Exception $e) { |
14 |
|
15 |
echo "Message: " . $e->getMessage(). "\n\n"; |
16 |
echo "File: " . $e->getFile(). "\n\n"; |
17 |
echo "Line: " . $e->getLine(). "\n\n"; |
18 |
echo "Trace: \n" . $e->getTraceAsString(). "\n\n"; |
19 |
|
20 |
}
|
El código anterior produce esta salida:
1 |
|
2 |
Message: Email is invalid |
3 |
|
4 |
File: C:\wamp\www\test\validator.php |
5 |
|
6 |
Line: 7 |
7 |
|
8 |
Trace: |
9 |
#0 C:\wamp\www\test\user.php(11): Validator->validate_email('$!%#$%#*')
|
10 |
#1 C:\wamp\www\test\test.php(12): User->save() |
11 |
#2 {main}
|
Por lo tanto, sin mirar una sola línea de código, podemos decir de dónde proviene la excepción. Podemos ver el nombre del archivo, el número de línea, el mensaje de excepciones y más. Los datos de rastreo incluso muestran las líneas exactas de código que se ejecutaron.
La estructura de la clase de excepción predeterminada se muestra en el manual de PHP, donde puede ver todos los métodos y datos con los que se incluye:



5 Extendiendo Excepciones
Dado que este es un concepto orientado a objetos y la excepción es una clase, podemos extenderlo para crear nuestras propias excepciones personalizadas.
Por ejemplo, es posible que no desee mostrar todos los detalles de una excepción al usuario. En su lugar, puede mostrar un mensaje fácil de usar y registrar el mensaje de error internamente:
1 |
|
2 |
// to be used for database issues
|
3 |
class DatabaseException extends Exception { |
4 |
|
5 |
// you may add any custom methods
|
6 |
public function log() { |
7 |
|
8 |
// log this error somewhere
|
9 |
// ...
|
10 |
}
|
11 |
}
|
12 |
|
13 |
// to be used for file system issues
|
14 |
class FileException extends Exception { |
15 |
|
16 |
// ...
|
17 |
|
18 |
}
|
Acabamos de crear dos nuevos tipos de excepciones. Y pueden tener métodos personalizados.
Cuando detectamos la excepción, podemos mostrar un mensaje fijo y llamar a los métodos personalizados internamente:
1 |
|
2 |
function foo() { |
3 |
|
4 |
// ...
|
5 |
// something wrong happened with the database
|
6 |
throw new DatabaseException(); |
7 |
|
8 |
}
|
9 |
|
10 |
|
11 |
try { |
12 |
|
13 |
// put all your code here
|
14 |
// ...
|
15 |
|
16 |
foo(); |
17 |
|
18 |
} catch (FileException $e) { |
19 |
|
20 |
die ("We seem to be having file system issues. |
21 |
We are sorry for the inconvenience."); |
22 |
|
23 |
} catch (DatabaseException $e) { |
24 |
|
25 |
// calling our new method
|
26 |
$e->log(); |
27 |
|
28 |
// exit with a message
|
29 |
die ("We seem to be having database issues. |
30 |
We are sorry for the inconvenience."); |
31 |
|
32 |
} catch (Exception $e) { |
33 |
|
34 |
echo 'Caught exception: '. $e->getMessage(). "\n"; |
35 |
|
36 |
}
|
Esta es la primera vez que vemos un ejemplo con varios bloques catch para un solo bloque try. Así es como puede capturar diferentes tipos de Excepciones, de modo que puede manejarlas de manera diferente.
En este caso, capturaremos una DatabaseException y solo ese bloque catch se ejecutará. En este blog podemos llamar a nuestros nuevos métodos personalizados y mostrar un mensaje simple al usuario.
Tenga en cuenta que el bloque catch con la clase de excepción predeterminada debe ser el último, ya que nuestras nuevas clases secundarias también se consideran esa clase. Por ejemplo, 'DatabaseException' también se considera 'Exception' por lo que puede quedar atrapado allí si el orden es al revés.
6 Manejo de excepciones no detectadas
Es posible que no siempre desee buscar excepciones en todo su código, envolviendo todo en bloques try-catch. Sin embargo, las excepciones no detectadas muestran un mensaje de error detallado para el usuario, que tampoco es ideal en un entorno de producción.
En realidad, hay una forma de centralizar el manejo de todas las excepciones no detectadas para que pueda controlar la salida desde una única ubicación.
Para esto, vamos a utilizar la función set_exception_handler ():
1 |
|
2 |
set_exception_handler('exception_handler'); |
3 |
|
4 |
function exception_handler($e) { |
5 |
|
6 |
// public message
|
7 |
echo "Something went wrong.\n"; |
8 |
|
9 |
// semi-hidden message
|
10 |
echo "<!-- Uncaught exception: " . $e->getMessage(). " -->\n"; |
11 |
|
12 |
}
|
13 |
|
14 |
|
15 |
throw new Exception('Hello.'); |
16 |
throw new Exception('World.'); |
La primera línea le indica a PHP que llame a una función dada cuando ocurre una excepción y no se detecta. Esta es la salida:
1 |
|
2 |
Something went wrong. |
3 |
<!-- Uncaught exception: Hello. -->
|
Como se puede ver, el script abortó después de la primera excepción y no ejecutó la segunda. Este es el comportamiento esperado de las excepciones no capturadas.
Si desea que su secuencia de comandos continúe ejecutándose después de una excepción, tendría que usar un bloque try-catch en su lugar.
7 Construyendo una clase de excepción MySQL
Vamos a terminar este tutorial construyendo una clase de Excepción MySQL personalizada que tiene algunas características útiles y veremos cómo podemos usarla.
1 |
|
2 |
class MysqlException extends Exception { |
3 |
|
4 |
// path to the log file
|
5 |
private $log_file = 'mysql_errors.txt'; |
6 |
|
7 |
|
8 |
public function __construct() { |
9 |
|
10 |
$code = mysql_errno(); |
11 |
$message = mysql_error(); |
12 |
|
13 |
// open the log file for appending
|
14 |
if ($fp = fopen($this->log_file,'a')) { |
15 |
|
16 |
// construct the log message
|
17 |
$log_msg = date("[Y-m-d H:i:s]") . |
18 |
" Code: $code - " . |
19 |
" Message: $message\n"; |
20 |
|
21 |
fwrite($fp, $log_msg); |
22 |
|
23 |
fclose($fp); |
24 |
}
|
25 |
|
26 |
// call parent constructor
|
27 |
parent::__construct($message, $code); |
28 |
}
|
29 |
|
30 |
}
|
Puede notar que colocamos casi todo el código en el constructor. Cada vez que se lanza una excepción, es como crear un nuevo objeto, por lo que siempre se llama primero al constructor. Al final del constructor, también nos aseguramos de llamar al constructor principal.
Esta excepción será lanzada siempre que encontremos un error de MySQL. Luego buscará el número de error y el mensaje directamente desde mysql, y luego almacenará esa información en un archivo de registro, junto con la marca de tiempo. En nuestro código podemos capturar esta excepción, mostrar un mensaje simple al usuario y dejar que la clase de excepción maneje el registro por nosotros.
Por ejemplo, intentemos conectarnos a MySQL sin proporcionar información de usuario / contraseña:
1 |
|
2 |
try { |
3 |
|
4 |
// attempt to connect
|
5 |
if (!@mysql_connect()) { |
6 |
throw new MysqlException; |
7 |
}
|
8 |
|
9 |
} catch (MysqlException $e) { |
10 |
|
11 |
die ("We seem to be having database issues. |
12 |
We are sorry for the inconvenience."); |
13 |
|
14 |
}
|
Necesitamos anteponer el operador de supresión de errores (@) antes de la llamada mysql_connect () para que no muestre el error al usuario. Si la función falla, lanzamos una excepción y luego la detectamos. Solo nuestro mensaje amigable será mostrado al navegador.
La clase MysqlException se encarga del registro de errores automáticamente. Cuando abra el archivo de registro, encontrará esta línea:
1 |
|
2 |
[2010-05-05 21:41:23] Code: 1045 - Message: Access denied for user 'SYSTEM'@'localhost' (using password: NO) |
Agreguemos más código a nuestro ejemplo y también proporcionemos una información de inicio de sesión correcta:
1 |
|
2 |
try { |
3 |
|
4 |
// connection should work fine
|
5 |
if (!@mysql_connect('localhost','root','')) { |
6 |
throw new MysqlException; |
7 |
}
|
8 |
|
9 |
// select a database (which may not exist)
|
10 |
if (!mysql_select_db('my_db')) { |
11 |
throw new MysqlException; |
12 |
}
|
13 |
|
14 |
// attempt a query (which may have a syntax error)
|
15 |
if (!$result = mysql_query("INSERT INTO foo SET bar = '42 ")) { |
16 |
throw new MysqlException; |
17 |
}
|
18 |
|
19 |
|
20 |
} catch (MysqlException $e) { |
21 |
|
22 |
die ("We seem to be having database issues. |
23 |
We are sorry for the inconvenience."); |
24 |
|
25 |
}
|
Si la conexión de la base de datos es exitosa, pero falta la base de datos llamada 'my_db', encontrará esto en los registros:
1 |
|
2 |
[2010-05-05 21:55:44] Code: 1049 - Message: Unknown database 'my_db' |
Si la base de datos está allí, pero la consulta falla, por ejemplo, debido a un error de sintaxis, puede ver esto en el registro:
1 |
|
2 |
[2010-05-05 21:58:26] Code: 1064 - Message: You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ''42' at line 1 |
Bonificación Construyendo una clase DB con PHP Magic
Podemos hacer que el ejemplo de código de arriba sea incluso más limpio al escribir nuestra propia clase de base de datos, que maneja el lanzamiento de las excepciones. Esta vez voy a usar algunas características "mágicas" de PHP para construir esta clase.
Al final, queremos que nuestro código principal se vea así:
1 |
|
2 |
try { |
3 |
|
4 |
Database::connect('localhost','root',''); |
5 |
|
6 |
Database::select_db('test'); |
7 |
|
8 |
$result = Database::query("INSERT INTO foo SET bar = '42 "); |
9 |
|
10 |
} catch (MysqlException $e) { |
11 |
|
12 |
die ("We seem to be having database issues. |
13 |
We are sorry for the inconvenience."); |
14 |
|
15 |
}
|
Es agradable y limpio. No verificamos errores en cada llamada a la base de datos. Esta nueva clase de base de datos tiene la responsabilidad de lanzar excepciones cuando ocurren errores. Y dado que estas excepciones se desvanecen, quedan atrapadas por nuestro bloque de captura al final.
Y aquí está la clase de base de datos mágica:
1 |
|
2 |
class Database { |
3 |
|
4 |
// any static function call invokes this
|
5 |
public static function __callStatic($name, $args) { |
6 |
|
7 |
// function to be called
|
8 |
$function = 'mysql_' . $name; |
9 |
|
10 |
// does the function exist?
|
11 |
if (!function_exists($function)) { |
12 |
// throw a regular exception
|
13 |
throw new Exception("Invalid mysql function: $function."); |
14 |
}
|
15 |
|
16 |
// call the mysql function
|
17 |
$ret = @call_user_func_array($function , $args); |
18 |
|
19 |
// they return FALSE on errors
|
20 |
if ($ret === FALSE) { |
21 |
// throw db exception
|
22 |
throw new MysqlException; |
23 |
}
|
24 |
|
25 |
// return the returned value
|
26 |
return $ret; |
27 |
}
|
28 |
|
29 |
}
|
Solo tiene un método y se invoca cuando llamamos a un método estático en esta clase. Puede leer más sobre este concepto aquí: Sobrecarga de PHP.
Entonces, cuando llamamos a Database :: connect (), este código llama automáticamente a mysql_connect (), pasa los argumentos, verifica errores, lanza excepciones cuando es necesario y devuelve el valor devuelto de la llamada a la función. Básicamente actúa como un intermediario, y maneja el trabajo sucio.
Conclusión
Espero que hayan disfrutado este tutorial y aprendido de él. Ahora deberías tener una mejor comprensión de este tema. Pruebe y vea si puede utilizar Excepciones de PHP en su próximo proyecto. ¡Nos vemos la próxima vez!



