try, catch и throw
В прошлом уроке брошенный объект DivisionByZeroError долетел до самого верха, и программа остановилась. Вспомним курьера: посылка, которую не вручили, идёт вверх по лестнице, пока кто-нибудь не откроет дверь. В коде такая дверь — блок catch, и поставить её можно там, где вы знаете, что делать со сбоем.
Бросать объекты умеет не только PHP, но и ваш код. Объект, который код бросает, чтобы сообщить о сбое, называется исключением. Для этого есть готовые классы, например InvalidArgumentException — «неправильный аргумент».
throw: бросить исключение
Пусть функция снимает деньги со счёта и не может уйти в минус:
<?php
function withdraw(int $balance, int $amount): int
{
if ($amount > $balance) {
throw new InvalidArgumentException('not enough money');
}
return $balance - $amount;
}
try {
echo withdraw(100, 30), "\n"; // 70
echo withdraw(100, 500), "\n";
} catch (InvalidArgumentException $e) {
echo 'error: ', $e->getMessage(), "\n"; // error: not enough money
}Разберём по шагам:
throw new InvalidArgumentException('not enough money')создаёт объект с сообщением и бросает его.- Первый вызов проходит проверку и возвращает
70. - Во втором вызове условие истинно.
throwпрерывает функцию: доreturnдело не доходит, и значения у вызова нет. - Исключение покидает
withdrawи попадает в код, который её вызвал. Строка сechoв блокеtryне завершается. - PHP проверяет
catch: класс подходит, объект попадает в переменную$e, и выполняется телоcatch.
Если бы вокруг вызова не было try, исключение пошло бы дальше: к тому, кто вызвал этот код, потом к следующему, и так до самого верха. Цепочку вызовов, которые ещё не закончились, называют стеком, а такой путь назад по ней — размоткой стека. На собеседовании любят спрашивать, что выполнится после throw: в блоке try — уже ничего, а после блока catch программа идёт дальше как обычно.
Блок catch
Пойманное исключение — это значение
В переменной $e лежит объект, и у него можно спросить подробности:
$e->getMessage()— сообщение, переданное при создании;$e->getCode()— числовой код ошибки, если его передали вторым аргументом, иначе0.
Первый подходящий catch
У одного try может быть несколько блоков catch. PHP проверяет их сверху вниз и выполняет первый, чей класс подходит:
<?php
try {
throw new RuntimeException('disk is full', 507);
} catch (InvalidArgumentException $e) {
echo 'bad argument', "\n";
} catch (Exception $e) {
echo $e->getMessage(), ' ', $e->getCode(), "\n"; // disk is full 507
}
echo 'next line', "\n"; // next lineRuntimeException не подходит под InvalidArgumentException, поэтому первый catch пропущен. Второй подходит: catch (Exception $e) ловит и класс Exception, и его дочерние классы, а RuntimeException и InvalidArgumentException — оба его потомки. Поэтому конкретные классы ставят выше, а общий Exception — ниже.
Здесь картинка с дверью перестаёт работать. Настоящую дверь откроют любому курьеру, а catch открывается только для своего класса и его потомков.
Несколько классов в одном catch
Когда разные сбои обрабатываются одинаково, их классы перечисляют через |:
<?php
$people = 0;
try {
echo intdiv(100, $people), "\n";
} catch (InvalidArgumentException | DivisionByZeroError $e) {
echo 'cannot share: ', $e->getMessage(), "\n"; // cannot share: Division by zero
}intdiv бросила DivisionByZeroError, и его поймал тот же catch, что ловит InvalidArgumentException. Вместо остановки программа напечатала сообщение.
Частая ошибка
После throw забывают слово new:
<?php
$name = '';
if ($name === '') {
throw InvalidArgumentException('empty name'); // ✗
}Fatal error: Uncaught Error: Call to undefined function InvalidArgumentException()Без new PHP читает InvalidArgumentException('empty name') как вызов функции с таким именем. Такой функции нет, и PHP бросает свою ошибку вместо вашей. Бросать можно только объект, поэтому нужно throw new InvalidArgumentException('empty name');.
Главное
throw new …бросает исключение: оставшиеся строки блокаtryи функции не выполняются.- Исключение идёт назад по цепочке вызовов до первого подходящего
catch, а послеcatchпрограмма продолжает работу. catch (Exception $e)ловитExceptionи все его дочерние классы; блокиcatchпроверяются сверху вниз.$e->getMessage()и$e->getCode()рассказывают, что случилось;catch (A | B $e)ловит два класса сразу.