Skip to content

7. Excepciones y errores

Cuando un método de una clase encuentra un error irrecuperable (archivo inexistente, base de datos sin conexión, conexión de red fuera de servicio), no muestra un error en una consola (archivo, base de datos), sino que lanza una excepción. Todas las excepciones heredan de la clase [\Exception]. Además de las excepciones, el funcionamiento interno de PHP también genera errores cuya clase base es la clase [\Error]. Ambas clases implementan la interfaz PHP [\Throwable].

7.1. El árbol de scripts

Image

7.2. La interfaz [\Throwable]

La interfaz [\Throwable] es la siguiente:

Image

La función de los métodos de la interfaz es la siguiente:

Image

7.3. Las excepciones predefinidas en PHP 7

PHP 7 definen varias clases de excepciones:

Image

  • en [1], las excepciones predefinidas en PHP;
  • En [2], las excepciones de la biblioteca SPL (Biblioteca estándar PHP) de PHP 7. La biblioteca SPL es una colección de clases e interfaces diseñadas para resolver problemas que enfrentan con frecuencia los desarrolladores.

7.4. Los errores predefinidos en PHP 7

PHP 7 define varias clases de errores:

Image

La clase [\Error] es la clase padre de todos los errores predefinidos en PHP. La clase [ErrorException] permite encapsular una instancia de la clase [\Error] dentro de una instancia de la clase [\Exception]. Esto permite estandarizar el manejo de errores al tratar únicamente las excepciones.

7.5. Ejemplo 1

El primer ejemplo, [exceptions-01.php], muestra tanto errores PHP como una excepción:


<?php

// visualización de todos los errores
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// código --------
$var=[];
// clave desconocida
print $var["abcd"];
// división por cero
$var=7/0;
var_dump($var);
// matriz con límites fijos
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// índice fuera de los límites
$array[5]=8;

Comentarios

  • línea 4: se le pide a PHP que reporte todos los errores. El segundo parámetro es el nivel de errores solicitado:

Image

Image

  • línea 5: se solicita que se muestren los errores en la consola;
  • línea 9: se accede a un elemento inexistente de la matriz [$var];
  • línea 11: se realiza una división por cero;
  • línea 14: se crea una instancia de la clase [SplFixedArray]. Esta clase permite crear un array de límites fijos e índices enteros;
  • línea 18: se accede a un elemento inexistente de la matriz;

Resultados

1
2
3
4
5
6
7
8
9
Notice: Undefined index: abcd in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 9

Warning: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 11
float(INF)

Fatal error: Uncaught RuntimeException: Index invalid or out of range in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php:18
Stack trace:
#0 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-01.php on line 18

Comentarios

  • línea 1 de los resultados: acceder a una clave inexistente de un array provoca un error PHP de nivel [E_NOTICE]. Esto no interrumpe la ejecución del script;
  • línea 3 de los resultados: dividir un número entre cero provoca un error PHP de nivel [E_WARNING]. Esto no interrumpe la ejecución del script;
  • líneas 6-9 de los resultados: acceder a un índice inexistente de un array [SplFixedArray] provoca una excepción de tipo [RuntimeException] e interrumpe la ejecución del script;

7.6. Manejo de excepciones

El script [exceptions-02.php] muestra cómo manejar las excepciones:


<?php

// se muestran todos los errores
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// se envuelve el código en un try / catch
try {
  $var = [];
  // clave desconocida
  print $var["abcd"];
  // división por cero
  $var = 7 / 0;
  var_dump($var);
  // matriz con límites fijos
  $array = new \SplFixedArray(5);
  $array[1] = 2;
  $array[4] = "foo";
  // índice fuera de los límites
  $array[5] = 8;
  // verificación
  print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
  // \Throwable es la interfaz implementada por la mayoría de los errores y excepciones
  // se muestra la excepción
  print "erreur, message : " . $ex->getMessage() . ", type : " . get_class($ex) . "\n";
}

Comentarios

  • el script es el que se presentó en el párrafo anterior. Solo que ahora se ha rodeado el código de las líneas 8 a 19, que podría provocar errores, con una estructura try / catch: si el código de las líneas 8 a 21 provoca (lanza) una excepción o un error, este será manejado por la cláusula catch de las líneas 22 a 26;
  • línea 22: el parámetro de la cláusula [catch] es el tipo de excepción o error que queremos manejar. Al establecer como tipo [\Throwable], que es una interfaz, se indica que se desea manejar cualquier instancia de clase que implemente la interfaz [\Throwable]. Dado que todas las clases de errores y excepciones implementan esta interfaz, la cláusula [catch] maneja aquí cualquier error o excepción encapsulada en una clase;
  • línea 19: la instrucción que provoca el error y genera la excepción. Tan pronto como se produce una excepción, se realiza una ramificación a la cláusula [catch]. Por lo tanto, el código que sigue a la línea 19 no se ejecutará;

Resultados

1
2
3
4
5
Notice: Undefined index: abcd in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-02.php on line 10

Warning: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-02.php on line 12
float(INF)
erreur, message : Index invalid or out of range, type : RuntimeException

Comentarios sobre los resultados

  • líneas 1 y 3: se observan los errores de nivel [E_NOTICE] y [E_WARNING]. Estos errores no son excepciones y, por lo tanto, no son manejados por la cláusula [catch];
  • línea 5: aparece el mensaje de error en la cláusula [catch]. Por lo tanto, se produjo una excepción derivada de [\Exception] o un error derivado de [\Error]. Aquí vemos que se trata de la clase [\RuntimeException];

7.7. Parámetros de la cláusula [catch]

Analicemos el siguiente script [exceptions-03.php]:


<?php

// se muestran todos los errores
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");

// una matriz con límites fijos
$array = new \SplFixedArray(5);
try {
  // índice fuera de los límites
  $array[5] = 8;
} catch (\Throwable $ex) {
  // muestra un mensaje de error
  print "Erreur 1 : " . $ex->getMessage() . "\n";
}

try {
  // índice fuera de los límites
  $array[5] = 8;
} catch (\Exception $ex) {
  // visualización de mensaje de error
  print "Erreur 2 : " . $ex->getMessage() . "\n";
}

try {
  // valor fuera de rango
  $array[5] = 8;
} catch (\RuntimeException $ex) {
  // visualización de mensaje de error
  print "Erreur 3 : " . $ex->getMessage() . "\n";
}
try {
  // división por 0
  intdiv(5, 0);
} catch (\Throwable $ex) {
  // mensaje de error
  print "Erreur 4 : " . $ex->getMessage() . "\n";
}

try {
  // división por 0
  intdiv(5, 0);
} catch (\DivisionByzeroError $ex) {
  // Muestra un mensaje de error
  print "Erreur 5 : " . $ex->getMessage() . "\n";
}

try {
  // división por 0
  intdiv(5, 0);
} catch (\Error $ex) {
  // visualización de mensaje de error
  print "Erreur 6 : " . $ex->getMessage() . "\n";
}

try {
  // división por 0
  intdiv(5, 0);
} catch (\Exception $ex) {
  // visualización de mensaje de error
  print "Erreur 6 : " . $ex->getMessage() . "\n";
}

Comentarios

  • líneas 8-31: tres formas diferentes de manejar la excepción generada por el uso de un índice incorrecto con la clase [\SplFixedArray]. Hemos visto que este error genera una excepción de tipo [RuntimeException];
    • línea 12: maneja un error de tipo [\Throwable]. Esto es válido, ya que el tipo [RuntimeException] deriva del tipo [\Exception], el cual implementa la interfaz [\Throwable];
    • línea 20: maneja un error de tipo [\Exception]. Esto es válido, ya que el tipo [RuntimeException] deriva del tipo [\Exception];
    • línea 28: maneja un error de tipo [\RuntimeException]. Este es el método preferido, ya que es el tipo exacto de la excepción generada;
  • líneas 32-62: cuatro formas diferentes de manejar la excepción generada por la función [intdiv] cuando se le pasa un divisor igual a 0. La función [ intdiv ( int $dividend , int $divisor ) : int] realiza la división entera $dividend / $divisor. Cuando el divisor es cero, se lanza la excepción [\DivisionByzeroError];
    • línea 35: se intercepta cualquier error que implemente la interfaz [\Throwable]. Esto es válido;
    • línea 43: se intercepta el tipo exacto del error: este es el método preferido;
    • línea 51: se intercepta el tipo [\Error]. Esto es válido, ya que la clase [DivisionByzeroError] extiende la clase [Error];
    • línea 59: se intercepta el tipo [\Exception]. Esto no es válido porque la clase [DivisionByzeroError] no tiene ninguna relación con la clase [\Exception];

Resultados

Erreur 1 : Index invalid or out of range
Erreur 2 : Index invalid or out of range
Erreur 3 : Index invalid or out of range
Erreur 4 : Division by zero
Erreur 5 : Division by zero
Erreur 6 : Division by zero

Fatal error: Uncaught DivisionByZeroError: Division by zero in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php:58
Stack trace:
#0 C:\Data\st-2019\dev\php7\php5-ejemplos\ejemplos\excepciones\exceptions-03.php(58): intdiv(5, 0)
#1 {main}
thrown in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-03.php on line 58

7.8. Cláusula [finally]

La estructura try / catch puede tener un tercer elemento y convertirse en una estructura try / catch / finally. El código de la cláusula [finally] se ejecuta en los dos casos siguientes:

  • la cláusula [try] no lanza una excepción. En ese caso, se ejecuta por completo y luego la ejecución del código pasa a la cláusula [finally], la cual se ejecuta por completo;
  • la cláusula [try] genera una excepción. En ese caso, se ejecuta hasta la instrucción que genera la excepción. La ejecución del código pasa entonces a la cláusula [catch], que se ejecuta por completo. Luego, la ejecución del código pasa a la cláusula [finally], que se ejecuta por completo;

Finalmente, el código de la cláusula [finally] se ejecuta de todos modos. Este escenario resulta útil en el siguiente caso:

  • en la cláusula [try], el código ha obtenido recursos (archivos, bases de datos, conexiones de red, colas). Por lo general, estos recursos consumen mucha memoria. Por lo tanto, es necesario liberarlos (lo que comúnmente se denomina «cerrarlos») tan pronto como sea posible;
  • si la adquisición de los recursos se realizó en el [try], su liberación se llevará a cabo en el [finally]. Esto garantiza que, en todos los casos (haya error o no), los recursos adquiridos se devuelvan al sistema;

El siguiente script, [exemples/exceptions/exceptions-04.php], nos muestra cómo funciona la cláusula [finally] en diversas situaciones:


<?php

// o crea una instancia de excepción
$e = new \Exception("Erreur…");    
var_dump($e);

// primera prueba
try {
  print "Premier test\n";
  throw $e;
} catch (\Exception $ex1) {
  print $ex1->getMessage() . "\n";
} finally {
  print "Terminé\n";
}

// segunda prueba
try {
  print "Second test\n";
} catch (\Exception $ex1) {
  print $ex1->getMessage() . "\n";
} finally {
  print "Terminé\n";
}

// tercera prueba
try {
  print "Troisième test\n";
  return;
} catch (\Exception $ex1) {
  print $ex1->getMessage() . "\n";
} finally {
  print "Terminé\n";
}

Comentarios del código

  • línea 4: $e es una instancia de la clase predefinida [\Exception]. La ejecutaremos en diferentes lugares;
  • líneas 8-15: la excepción $e se lanza en [try] (línea 10);
  • línea 11: se intercepta la excepción [\Exception] y se escribe su mensaje de error en la consola;
  • líneas 13-15: la cláusula [finally] escribe un mensaje. Según lo dicho anteriormente, este mensaje debería escribirse siempre, haya o no un error en [try];
  • líneas 18-24: no hay error en el [try]. Aquí también se debería pasar al [finally];
  • líneas 27-34: hay una instrucción [return] en el try y no hay error. Entonces, cabe preguntarse si se pasará a la cláusula [finally]. La ejecución muestra que sí;

Resultados

1
2
3
4
5
6
7
Premier test
Erreur…
Terminé
Second test
Terminé
Troisième test
Terminé

Analicemos otro caso, [exceptions-05.php]:


<?php

// cuarta prueba
try {
  print "Quatrième test\n";
  exit;
} finally {
  print "Terminé\n";
}

Comentarios

  • línea 6: la instrucción [exit] detiene inmediatamente la ejecución del script: la cláusula [finally] no se ejecuta;
  • líneas 4-9: un ejemplo de try / catch / finally sin la cláusula [catch]. Es posible;

Resultados

Quatrième test

7.9. Crear tus propias clases de excepciones

En un proyecto de cierta envergadura, resulta útil diferenciar los distintos errores encapsulándolos en diferentes clases de excepciones. En el script anterior, vimos que cualquier excepción podía ser interceptada mediante una cláusula [catch (\Throwable]. Esto se recomienda si no se tiene idea del error interceptado y el tratamiento es el mismo para todos los errores. A veces es así, pero con frecuencia se necesita adaptar el tratamiento al tipo exacto del error. Por lo tanto, es necesario diferenciar los errores entre sí.

Analicemos el siguiente script [exceptions-06.php]:


<?php

// definimos nuestra propia familia de excepciones
class Exception1 extends \RuntimeException {
  
}

class Exception2 extends \RuntimeException {
  
}

// o utilizamos nuestras excepciones
$e1 = new Exception1("Erreur1…");
var_dump($e1);
$e2 = new Exception2("Erreur2…");
var_dump($e2);

// primera prueba
print ("premier test\n");
try {
  // lanzamos un tipo Exception1
  throw $e1;
} catch (Exception1 $ex1) {
  print "Exception 1" . "\n";
  print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
  print "Exception 2" . "\n";
  print $ex2->getMessage() . "\n";
}

// segunda prueba
print ("second test\n");
try {
  // lanzamos un tipo Exception2
  throw $e2;
} catch (Exception1 $ex1) {
  print "Exception 1" . "\n";
  print $ex1->getMessage() . "\n";
} catch (Exception2 $ex2) {
  print "Exception 2" . "\n";
  print $ex2->getMessage() . "\n";
}

// tercera prueba
print ("troisième test\n");
try {
  // se lanza un tipo Exception1
  throw $e1;
} catch (Exception1 | Exception2 $ex) {
  print "Exception 1 ou 2" . "\n";
  print $ex->getMessage() . "\n";
}

// cuarta prueba
print ("quatrième test\n");
try {
  // se lanza un tipo Exception2
  throw $e2;
} catch (Exception1 | Exception2 $ex) {
  print "Exception 1 ou 2" . "\n";
  print $ex->getMessage() . "\n";
}

Comentarios

  • líneas 4-10: se definen dos clases, [Exception1] y [Exception2], ambas derivadas de la clase predefinida [\RuntimeException]. El cuerpo de estas clases está vacío. En otras palabras, solo se utilizan por sus tipos: es precisamente porque tienen tipos diferentes que podremos diferenciar estas dos excepciones en las cláusulas [catch];
  • líneas 13-16: se definen dos variables, $e1 y $e2, que tienen, respectivamente, los tipos [Exception1] y [Exception2];
  • líneas 20-29: se utiliza una estructura try / catch / catch. Esto permite manejar diferentes tipos de excepciones con diferentes cláusulas [catch];
  • línea 23: intercepta las excepciones de tipo [Exception1];
  • línea 26: intercepta las excepciones de tipo [Exception2];
  • línea 49: intercepta las excepciones de tipo [Exception1] o (|) [Exception2];

Resultados

object(Exception1)#1 (7) {
  ["message":protected]=>
  string(10) "Erreur1…"
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-06.php"
  ["line":protected]=>
  int(13)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  NULL
}
object(Exception2)#2 (7) {
  ["message":protected]=>
  string(10) "Erreur2…"
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(0)
  ["file":protected]=>
  string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-06.php"
  ["line":protected]=>
  int(15)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  NULL
}
premier test
Exception 1
Erreur1…
second test
Exception 2
Erreur2…
troisième test
Exception 1 ou 2
Erreur1…
quatrième test
Exception 1 ou 2
Erreur2…

Comentarios sobre los resultados

  • líneas 1-17: el «contenido» de una excepción:
    • líneas 2-3: el mensaje de error;
    • líneas 6-7: el código de error;
    • líneas 8-9: el nombre del archivo en el que ocurrió la excepción;
    • líneas 10-11: la línea en la que ocurrió la excepción;
    • líneas 15-16: la excepción anterior. Una excepción puede encapsular otra excepción y definir así una pila de excepciones. El atributo [previous] permitirá aprovechar esta pila;

7.10. Relanzar una excepción

Una excepción puede lanzarse varias veces, como lo muestra el siguiente script [exceptions-07.php]:


<?php

try {
  try {
    // se lanza una excepción
    throw new \Exception("test");
  } catch (\Exception $ex) {
    // se vuelve a lanzar la excepción interceptada
    throw $ex;
  } finally {
    // se ejecutará correctamente el bloque finally
    print "finally 1\n";
  }
} catch (\Exception $ex2) {
  // se recupera correctamente la excepción inicial
  print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
  // se ejecutará correctamente el bloque finally
  print "finally 2\n";
}

Comentarios

  • línea 6: se lanza una excepción;
  • línea 7: se intercepta;
  • línea 9: se vuelve a lanzar. Entonces pasa a la estructura try / catch / finally del nivel superior;
  • línea 14: se intercepta nuevamente;
  • líneas 10-12: la ejecución muestra que, incluso después del [throw] de la línea 9, se pasa correctamente a la cláusula [finally] del try / catch / finally;

Resultados

1
2
3
finally 1
test dans try / catch / finally externe
finally 2

7.11. Aprovechamiento de una pila de excepciones

Una excepción puede encapsular otra excepción, la cual a su vez puede encapsular otra, formando finalmente una pila de excepciones. A continuación se muestra un ejemplo [exceptions-08.php]:

<?php

// definimos nuestra propia familia de excepciones
class Exception1 extends \RuntimeException {

}

class Exception2 extends \RuntimeException {

}

class Exception3 extends \RuntimeException {

}

// o utilizamos nuestras excepciones
$e1 = new Exception1("Erreur 1…", 1, new Exception2("Erreur 2…", 2, new Exception3("Erreur 3…")));
var_dump($e1);
// manejo de la excepción actual
print $e1->getMessage() . "\n";
$e = $e1;
while ($e->getPrevious() !== NULL) {
   // excepción anterior
  $e = $e->getPrevious();
   // mensaje de error
  print $e->getMessage() . "\n";
}

Comentarios

  • líneas 4-14: definen tres clases de excepciones derivadas de la excepción predefinida [RuntimeException];
  • línea 17: una instancia de la clase [Exception3] está encapsulada en una instancia de la clase [Exception2], la cual a su vez está encapsulada en una instancia de la clase [Exception1]. El constructor utilizado aquí es el de la clase [Exception]:

Image

El tercer parámetro del constructor permite encapsular una excepción. Esto puede ser útil en el siguiente escenario:

  • se define un método M que puede generar una excepción de tipo [Exception1] y solo de este tipo, por razones de compatibilidad, por ejemplo, con una interfaz;
  • sin embargo, en el método M pueden ocurrir otros tipos de excepciones. Para reportar un error al código que llama al método M, se encapsularán estas excepciones en el tipo [Exception1], que luego se lanzará. Esto permite no perder la información contenida en la excepción encapsulada, la cual fue la causa original del error;
  • las líneas 20-27 muestran cómo manejar la pila de excepciones internas de una excepción;

Resultados


object(Exception1)#1 (7) {
  ["message":protected]=>
  string(11) "Erreur 1…"
  ["string":"Exception":private]=>
  string(0) ""
  ["code":protected]=>
  int(1)
  ["file":protected]=>
  string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
  ["line":protected]=>
  int(17)
  ["trace":"Exception":private]=>
  array(0) {
  }
  ["previous":"Exception":private]=>
  object(Exception2)#2 (7) {
    ["message":protected]=>
    string(11) "Erreur 2…"
    ["string":"Exception":private]=>
    string(0) ""
    ["code":protected]=>
    int(2)
    ["file":protected]=>
    string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
    ["line":protected]=>
    int(17)
    ["trace":"Exception":private]=>
    array(0) {
    }
    ["previous":"Exception":private]=>
    object(Exception3)#3 (7) {
      ["message":protected]=>
      string(11) "Erreur 3…"
      ["string":"Exception":private]=>
      string(0) ""
      ["code":protected]=>
      int(0)
      ["file":protected]=>
      string(76) "C:\Data\st-2019\dev\php7\php5-exemples\exemples\exceptions\exceptions-08.php"
      ["line":protected]=>
      int(17)
      ["trace":"Exception":private]=>
      array(0) {
      }
      ["previous":"Exception":private]=>
      NULL
    }
  }
}
Erreur 1…
Erreur 2…
Erreur 3…