Skip to content

7. Винятки та помилки

Коли метод класу стикається з непереборною помилкою (файл не існує, з базою даних не встановлено з’єднання, мережеве з’єднання не працює), він не виводить повідомлення про помилку на консоль (файл, база даних), а генерує виняток. Усі винятки успадковують клас [\Exception]. Окрім винятків, внутрішня робота PHP також генерує помилки, базовим класом яких є клас [\Error]. Обидва класи реалізують інтерфейс PHP [\Throwable].

7.1. Дерево скриптів

Image

7.2. Інтерфейс [\Throwable]

Інтерфейс [\Throwable] має такий вигляд:

Image

Роль методів інтерфейсу така:

Image

7.3. Визначені заздалегідь винятки в PHP 7

PHP 7 визначають кілька класів винятків:

Image

  • у [1] — це заздалегідь визначені винятки в PHP;
  • у [2] — винятки з бібліотеки SPL (стандартна бібліотека PHP) з PHP 7. Бібліотека SPL — це набір класів та інтерфейсів, призначених для вирішення проблем, з якими часто стикаються розробники.

7.4. Помилки, заздалегідь визначені в PHP 7

PHP 7 визначає кілька класів помилок:

Image

Клас [\Error] є батьківським класом для всіх попередньо визначених помилок у PHP. Клас [ErrorException] дозволяє інкапсулювати екземпляр класу [\Error] в екземпляр класу [\Exception]. Це дозволяє уніфікувати обробку помилок, обробляючи лише винятки.

7.5. Приклад 1

У першому прикладі [exceptions-01.php] показано як помилки PHP, так і виняток:


<?php

// виведення всіх помилок
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// код --------
$var=[];
// невідомий ключ
print $var["abcd"];
// ділення на нуль
$var=7/0;
var_dump($var);
// масив з фіксованими межами
$array = new \SplFixedArray(5);
$array[1] = 2;
$array[4] = "foo";
// індекс поза межами діапазону
$array[5]=8;

Коментарі

  • рядок 4: PHP отримує завдання повідомляти про всі помилки. Другий параметр — це заданий рівень помилок:

Image

Image

  • рядок 5: задається виведення помилок на консоль;
  • рядок 9: здійснюється доступ до неіснуючого елемента масиву [$var];
  • рядок 11: виконується ділення на нуль;
  • рядок 14: створюється екземпляр класу [SplFixedArray]. Цей клас дозволяє створити масив з фіксованими межами та цілочисельними індексами;
  • рядок 18: здійснюється доступ до елемента масиву, який не існує;

Результати

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

Коментарі

  • рядок 1 результатів: звернення до неіснуючого ключа масиву спричиняє помилку PHP рівня [E_NOTICE]. Це не перериває виконання скрипта;
  • 3-й рядок результатів: ділення числа на нуль спричиняє помилку PHP рівня [E_WARNING]. Це не перериває виконання скрипта;
  • рядки 6–9 результатів: звернення до неіснуючого індексу масиву [SplFixedArray] спричиняє виняток типу [RuntimeException] і перериває виконання скрипта;

7.6. Обробка винятків

Скрипт [exceptions-02.php] демонструє, як обробляти винятки:


<?php

// виводити всі помилки
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");
// обгортання коду блоком try / catch
try {
  $var = [];
  // невідомий ключ
  print $var["abcd"];
  // ділення на нуль
  $var = 7 / 0;
  var_dump($var);
  // масив з фіксованими межами
  $array = new \SplFixedArray(5);
  $array[1] = 2;
  $array[4] = "foo";
  // індекс поза межами діапазону
  $array[5] = 8;
  // перевірка
  print "ce message ne sera pas affiché\n";
} catch (\Throwable $ex) {
  // \Throwable — це інтерфейс, реалізований більшістю помилок та винятків
  // виводиться виняток
  print "erreur, message : " . $ex->getMessage() . ", type : " . get_class($ex) . "\n";
}

Коментарі

  • цей скрипт є тим самим, що був представлений у попередньому параграфі. Тільки тепер код рядків 8–19, який може спричинити помилки, обгорнуто структурою try / catch: якщо код рядків 8–21 спричиняє (викликає) виняток або помилку, то вона буде оброблена за допомогою блоку catch у рядках 22–26;
  • рядок 22: параметром блоку catch є [catch] — тип винятку або помилки, який ми хочемо обробити. Вказавши як тип [\Throwable], який є інтерфейсом, ми вказуємо, що хочемо обробляти будь-який екземпляр класу, що реалізує інтерфейс [\Throwable]. Оскільки всі класи помилок та винятків реалізують цей інтерфейс, клаузула [catch] обробляє тут будь-яку помилку/виняток, інкапсульований у класі;
  • рядок 19: інструкція, яка викликає помилку та породжує виняток. Як тільки виникає виняток, відбувається перехід до клаузули [catch]. Отже, код після рядка 19 не буде виконано;

Результати

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

Коментарі до результатів

  • рядки 1 і 3: тут виявлені помилки рівнів [E_NOTICE] та [E_WARNING]. Ці помилки не є винятками, тому не обробляються за допомогою клаузули [catch];
  • рядок 5: тут міститься повідомлення про помилку, записане в клаузулі [catch]. Отже, сталося виключення, похідне від [\Exception], або помилка, похідна від [\Error]. Тут ми бачимо, що мова йде про клас [\RuntimeException];

7.7. Параметри клаузули [catch]

Розглянемо наступний скрипт [exceptions-03.php]:


<?php

// виводяться всі помилки
ini_set("error_reporting", E_ALL);
ini_set("display_errors", "on");

// масив з фіксованими межами
$array = new \SplFixedArray(5);
try {
  // індекс поза межами діапазону
  $array[5] = 8;
} catch (\Throwable $ex) {
  // виведення повідомлення про помилку
  print "Erreur 1 : " . $ex->getMessage() . "\n";
}

try {
  // індекс поза межами діапазону
  $array[5] = 8;
} catch (\Exception $ex) {
  // відображення повідомлення про помилку
  print "Erreur 2 : " . $ex->getMessage() . "\n";
}

try {
  // показник поза межами діапазону
  $array[5] = 8;
} catch (\RuntimeException $ex) {
  // виведення повідомлення про помилку
  print "Erreur 3 : " . $ex->getMessage() . "\n";
}
try {
  // ділення на 0
  intdiv(5, 0);
} catch (\Throwable $ex) {
  // виведення повідомлення про помилку
  print "Erreur 4 : " . $ex->getMessage() . "\n";
}

try {
  // ділення на 0
  intdiv(5, 0);
} catch (\DivisionByzeroError $ex) {
  // виведення повідомлення про помилку
  print "Erreur 5 : " . $ex->getMessage() . "\n";
}

try {
  // ділення на 0
  intdiv(5, 0);
} catch (\Error $ex) {
  // виведення повідомлення про помилку
  print "Erreur 6 : " . $ex->getMessage() . "\n";
}

try {
  // ділення на 0
  intdiv(5, 0);
} catch (\Exception $ex) {
  // виведення повідомлення про помилку
  print "Erreur 6 : " . $ex->getMessage() . "\n";
}

Коментарі

  • рядки 8–31: 3 різні способи обробки винятку, що виникає внаслідок використання неправильного індексу з класом [\SplFixedArray]. Ми бачили, що ця помилка генерує виняток типу [RuntimeException];
    • рядок 12: обробляє помилку типу [\Throwable]. Це допустимо, оскільки тип [RuntimeException] походить від типу [\Exception], який реалізує інтерфейс [\Throwable];
    • рядок 20: обробляє помилку типу [\Exception]. Це допустимо, оскільки тип [RuntimeException] походить від типу [\Exception];
    • рядок 28: обробляє помилку типу [\RuntimeException]. Цей підхід є кращим, оскільки це точний тип згенерованого винятку;
  • рядки 32–62: 4 різні способи обробки винятку, згенерованого функцією [intdiv], коли їй передається дільник, рівний 0. Функція [ intdiv ( int $dividend , int $divisor ) : int] виконує цілочисельне ділення $dividend / $divisor. Коли дільник дорівнює нулю, генерується виняток [\DivisionByzeroError];
    • рядок 35: перехоплюється будь-яка помилка, що реалізує інтерфейс [\Throwable]. Це правильно;
    • рядок 43: перехоплюється точний тип помилки: це найкращий підхід;
    • рядок 51: перехоплюється тип [\Error]. Це допустимо, оскільки клас [DivisionByzeroError] успадковує клас [Error];
    • рядок 59: перехоплюється тип [\Exception]. Це недійсне, оскільки клас [DivisionByzeroError] не має жодного зв’язку з класом [\Exception];

Результати

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-exemples\exemples\exceptions\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. Умова [finally]

Структура try / catch може мати третій елемент і перетворитися на структуру try / catch / finally. Код блоку [finally] виконується в обох наступних випадках:

  • блок [try] не генерує винятку. У цьому випадку він виконується повністю, після чого виконання коду переходить до блоку [finally], який також виконується повністю;
  • клаузула [try] генерує виняток. У цьому випадку вона виконується до інструкції, яка генерує виняток. Потім виконання коду переходить до блоку [catch], який виконується повністю. Далі виконання коду переходить до блоку [finally], який виконується повністю;

Нарешті, код розділу [finally] все одно виконується. Цей сценарій корисний у такому випадку:

  • у блоці [try] код отримав ресурси (файли, бази даних, мережеві з’єднання, черги). Зазвичай ці ресурси вимагають значних обсягів пам’яті. Тому їх потрібно повернути (найчастіше кажуть «закрити») щонайшвидше;
  • якщо отримання ресурсів відбулося у сценарії [try], їх повернення слід розмістити у сценарії [finally]. Це гарантує, що в будь-якому випадку (незалежно від наявності помилки) отримані ресурси будуть повернуті системі;

Наступний скрипт [exemples/exceptions/exceptions-04.php] демонструє роботу клаузули [finally] у різних ситуаціях:


<?php

// або створює екземпляр винятку
$e = new \Exception("Erreur…");    
var_dump($e);

// перший тест
try {
  print "Premier test\n";
  throw $e;
} catch (\Exception $ex1) {
  print $ex1->getMessage() . "\n";
} finally {
  print "Terminé\n";
}

// другий тест
try {
  print "Second test\n";
} catch (\Exception $ex1) {
  print $ex1->getMessage() . "\n";
} finally {
  print "Terminé\n";
}

// третій тест
try {
  print "Troisième test\n";
  return;
} catch (\Exception $ex1) {
  print $ex1->getMessage() . "\n";
} finally {
  print "Terminé\n";
}

Коментарі до коду

  • рядок 4: $e — це екземпляр попередньо визначеного класу [\Exception]. Ми запустимо його в різних місцях;
  • рядки 8–15: виняток $e генерується в [try] (рядок 10);
  • рядок 11: виняток [\Exception] перехоплюється, а його повідомлення про помилку записується в консоль;
  • рядки 13–15: оператор [finally] записує повідомлення. Згідно з тим, що було сказано раніше, це повідомлення має записуватися завжди, незалежно від того, чи є помилка в [try];
  • рядки 18–24: у [try] немає помилки. Тут також слід перейти до [finally];
  • рядки 27–34: у блоці try є інструкція [return], і помилки немає. Тож можна задатися питанням, чи відбудеться перехід до блоку [finally]. Виконання показує, що так;

Результати

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

Розглянемо інший випадок [exceptions-05.php]:


<?php

// четвертий тест
try {
  print "Quatrième test\n";
  exit;
} finally {
  print "Terminé\n";
}

Коментарі

  • рядок 6: оператор [exit] негайно зупиняє виконання скрипта: блок [finally] не виконується;
  • рядки 4–9: приклад використання try / catch / finally без оператора [catch]. Це можливо;

Результати

Quatrième test

7.9. Створення власних класів винятків

У досить великому проєкті корисно розрізняти різні помилки, інкапсулюючи їх у різні класи винятків. У попередньому скрипті ми бачили, що будь-який виняток можна перехопити за допомогою оператора [catch (\Throwable]. Це рекомендується, якщо немає уявлення про перехоплене виключення і обробка однакова для всіх помилок. Іноді так і буває, але часто потрібно адаптувати обробку до конкретного типу помилки. Тоді необхідно розрізняти помилки між собою.

Розглянемо наступний скрипт [exceptions-06.php]:


<?php

// визначаємо власну групу винятків
class Exception1 extends \RuntimeException {
  
}

class Exception2 extends \RuntimeException {
  
}

// або використовуємо наші винятки
$e1 = new Exception1("Erreur1…");
var_dump($e1);
$e2 = new Exception2("Erreur2…");
var_dump($e2);

// перший тест
print ("premier test\n");
try {
  // викликаємо тип 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";
}

// другий тест
print ("second test\n");
try {
  // викликаємо тип 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";
}

// третій тест
print ("troisième test\n");
try {
  // викликається тип Exception1
  throw $e1;
} catch (Exception1 | Exception2 $ex) {
  print "Exception 1 ou 2" . "\n";
  print $ex->getMessage() . "\n";
}

// четвертий тест
print ("quatrième test\n");
try {
  // викликається виняток типу Exception2
  throw $e2;
} catch (Exception1 | Exception2 $ex) {
  print "Exception 1 ou 2" . "\n";
  print $ex->getMessage() . "\n";
}

Коментарі

  • рядки 4–10: визначаються два класи [Exception1] та [Exception2], обидва похідні від попередньо визначеного класу [\RuntimeException]. Тіло цих класів порожнє. Іншими словами, вони використовуються лише заради їхніх типів: саме завдяки тому, що вони мають різні типи, ми зможемо розрізнити ці два винятки в умовах [catch];
  • рядки 13–16: ми визначаємо дві змінні $e1 та $e2, що мають відповідно типи [Exception1] та [Exception2];
  • рядки 20–29: використовується структура try / catch / catch. Це дозволяє обробляти різні типи винятків за допомогою різних блоків [catch];
  • рядок 23: перехоплює винятки типу [Exception1];
  • рядок 26: перехоплює винятки типу [Exception2];
  • рядок 49: перехоплює винятки типу [Exception1] або (|) [Exception2];

Результати

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…

Коментарі до результатів

  • рядки 1–17: «вміст» винятку:
    • рядки 2–3: повідомлення про помилку;
    • рядки 6–7: код помилки;
    • рядки 8–9: назва файлу, у якому сталося виключення;
    • рядки 10–11: рядок, у якому стався виняток;
    • рядки 15–16: попереднє виключення. Виключення може інкапсулювати інше виключення, утворюючи таким чином стек виключень. Атрибут [previous] дозволить використовувати цей стек;

7.10. Повторний запуск винятку

Виняток можна запустити кілька разів, як показано в наступному скрипті [exceptions-07.php]:


<?php

try {
  try {
    // викликається виняток
    throw new \Exception("test");
  } catch (\Exception $ex) {
    // перезапускається перехоплене виключення
    throw $ex;
  } finally {
    // виконання успішно перейде до блоку finally
    print "finally 1\n";
  }
} catch (\Exception $ex2) {
  // початкове виключення успішно отримано
  print $ex2->getMessage() . " dans try / catch / finally externe\n";
} finally {
  // виконання успішно перейде до блоку finally
  print "finally 2\n";
}

Коментарі

  • рядок 6: генерується виняток;
  • рядок 7: виняток перехоплюється;
  • рядок 9: виняток запускається знову. Після цього він потрапляє в структуру try / catch / finally вищого рівня;
  • рядок 14: його знову перехоплюють;
  • рядки 10–12: виконання показує, що навіть після [throw] у рядку 9 програма дійсно переходить до блоку [finally] конструкції try / catch / finally;

Результати

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

7.11. Використання стека винятків

Виняток може інкапсулювати інший виняток, який, у свою чергу, може інкапсулювати ще один, утворюючи в результаті стек винятків. Ось приклад [exceptions-08.php]:

<?php

// визначаємо власний клас винятків
class Exception1 extends \RuntimeException {

}

class Exception2 extends \RuntimeException {

}

class Exception3 extends \RuntimeException {

}

// або використовуємо наші винятки
$e1 = new Exception1("Erreur 1…", 1, new Exception2("Erreur 2…", 2, new Exception3("Erreur 3…")));
var_dump($e1);
// обробка поточного винятку
print $e1->getMessage() . "\n";
$e = $e1;
while ($e->getPrevious() !== NULL) {
   // попереднє виключення
  $e = $e->getPrevious();
   // повідомлення про помилку
  print $e->getMessage() . "\n";
}

Коментарі

  • рядки 4–14: визначають три класи винятків, похідні від попередньо визначеного винятку [RuntimeException];
  • рядок 17: екземпляр класу [Exception3] інкапсульований в екземпляр класу [Exception2], який, у свою чергу, інкапсульований в екземпляр класу [Exception1]. Тут використовується конструктор класу [Exception]:

Image

Третій параметр конструктора дозволяє інкапсулювати виняток. Це може бути корисно в такому сценарії:

  • визначається метод M, який може генерувати виняток типу [Exception1] і лише цього типу, наприклад, з міркувань сумісності з інтерфейсом;
  • проте в методі M можуть виникати й інші типи винятків. Щоб передати помилку коду, що викликає метод M, ці винятки інкапсулюються в тип [Exception1], який потім генерується. Це дозволяє не втратити інформацію, що міститься в інкапсульованому винятку і яка була першопричиною помилки;
  • рядки 20–27 показують, як керувати стеком винятків, що містяться всередині одного винятку;

Результати


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…