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

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

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

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

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

Клас [\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 отримує завдання повідомляти про всі помилки. Другий параметр — це заданий рівень помилок:


- рядок 5: задається виведення помилок на консоль;
- рядок 9: здійснюється доступ до неіснуючого елемента масиву [$var];
- рядок 11: виконується ділення на нуль;
- рядок 14: створюється екземпляр класу [SplFixedArray]. Цей клас дозволяє створити масив з фіксованими межами та цілочисельними індексами;
- рядок 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 і 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];
Результати
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]. Виконання показує, що так;
Результати
Розглянемо інший випадок [exceptions-05.php]:
<?php
// четвертий тест
try {
print "Quatrième test\n";
exit;
} finally {
print "Terminé\n";
}
Коментарі
- рядок 6: оператор [exit] негайно зупиняє виконання скрипта: блок [finally] не виконується;
- рядки 4–9: приклад використання try / catch / finally без оператора [catch]. Це можливо;
Результати
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];
Результати
Коментарі до результатів
- рядки 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;
Результати
7.11. Використання стека винятків
Виняток може інкапсулювати інший виняток, який, у свою чергу, може інкапсулювати ще один, утворюючи в результаті стек винятків. Ось приклад [exceptions-08.php]:
Коментарі
- рядки 4–14: визначають три класи винятків, похідні від попередньо визначеного винятку [RuntimeException];
- рядок 17: екземпляр класу [Exception3] інкапсульований в екземпляр класу [Exception2], який, у свою чергу, інкапсульований в екземпляр класу [Exception1]. Тут використовується конструктор класу [Exception]:
![]()
Третій параметр конструктора дозволяє інкапсулювати виняток. Це може бути корисно в такому сценарії:
- визначається метод 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…