finally и свои исключения
У курьера есть журнал обхода. Посылку вручили, оставили записку или понесли обратно — что бы ни случилось у двери, курьер расписывается в журнале. Без подписи никто не узнает, на какой квартире закончился обход.
В коде тоже бывают действия, которые нужны при любом исходе: закрыть файл, освободить ресурс, записать «готово». Если поставить такую строку после try и catch, она не выполнится, когда в try сработает return или исключение останется непойманным. Для этого у try есть третий блок — finally.
Блок finally
Блок finally пишут после try и блоков catch, а catch можно и не писать вовсе. PHP выполняет finally при любом выходе из try:
- Блок
tryдошёл до конца —finallyвыполняется следом. - В
tryилиcatchсработалreturn— значение откладывается, выполняетсяfinally, и только потом функция возвращает отложенное значение. - Брошено исключение —
finallyвыполняется после подходящегоcatch, а если такого нет, перед тем как исключение уйдёт выше.
Давайте проследим за функцией, которая выходит из try двумя путями: через return и через throw. Нажимайте «Следующий шаг» и следите, на каком шаге печатается finally и когда echo получает результат.
1<?php2 3function parse(string $s): int4{5 try {6 if ($s === '') {7 throw new Exception('empty');8 }9 return (int) $s;10 } catch (Exception $e) {11 echo 'caught', "\n";12 return 0;13 } finally {14 echo 'finally', "\n";15 }16}17 18echo parse('42'), "\n";19echo parse(''), "\n";
Чтобы напечатать результат, PHP вызывает parse('42').
Программа напечатала finally, 42, caught, finally, 0. В обоих вызовах finally сработал раньше, чем echo получил результат:
- Для
'42'операторreturnотложил42,finallyнапечатал свою строку, и только потомechoнапечатал число. - Для пустой строки
throwпрервалtry,catchнапечаталcaughtи отложил0, затем снова сработалfinally.
Частый вопрос на собеседовании: выполнится ли finally, если в try стоит return. Да: return откладывает значение, finally выполняется, и только потом функция его возвращает. Пропустить finally может настоящая фатальная ошибка, например нехватка памяти: она останавливает программу сразу.
Здесь картинка с журналом перестаёт работать. Курьер расписывается после доставки, а finally выполняется до того, как значение из return дойдёт до вызывающего кода.
Свои классы исключений
По тексту сообщения сбои трудно различать: текст меняют, а catch его не читает. Надёжнее дать сбою свой класс. Свой класс исключения наследует один из готовых классов, и пустого тела достаточно: конструктор с сообщением и кодом, getMessage и getCode достаются от родителя.
<?php
class OutOfStockException extends RuntimeException {}
function reserve(int $stock, int $wanted): int
{
if ($wanted > $stock) {
throw new OutOfStockException("only $stock left", 42);
}
return $stock - $wanted;
}Поймать такое исключение можно и по его классу, и по любому родителю:
try {
echo reserve(5, 2), "\n"; // 3
echo reserve(5, 9), "\n";
} catch (RuntimeException $e) {
echo get_class($e), "\n"; // OutOfStockException
echo $e->getCode(), ' ', $e->getMessage(), "\n"; // 42 only 5 left
}OutOfStockException — потомок RuntimeException, поэтому catch (RuntimeException $e) его ловит. Объект при этом не меняется: get_class($e) называет настоящий класс, а сообщение и код 42 остались те, что передали в new.
Передать дальше: throw $e
Иногда функция может отметить сбой, но не исправить его. Тогда в catch она записывает, что случилось, и бросает тот же объект снова — throw $e:
function buy(int $wanted): int
{
try {
return reserve(5, $wanted);
} catch (OutOfStockException $e) {
echo 'log: ', $e->getMessage(), "\n"; // log: only 5 left
throw $e;
}
}
try {
buy(9);
} catch (RuntimeException $e) {
echo 'Sorry, sold out', "\n"; // Sorry, sold out
}buy напечатала строку с log:, как будто записала в журнал, и отправила исключение дальше вверх. Его поймал внешний catch и показал покупателю понятное сообщение.
Частая ошибка
Свой класс исключения объявляют без extends:
<?php
class OutOfStockException {}
throw new OutOfStockException('only 5 left'); // ✗Fatal error: Uncaught Error: Cannot throw objects that do not implement ThrowableБросать можно только объект, который реализует Throwable, а обычный класс его не реализует. Добавьте родителя из ветви Exception: class OutOfStockException extends RuntimeException {}.
Главное
finallyвыполняется при любом выходе изtry: после конца блока, послеreturnи после исключения.- Значение из
returnждёт, покаfinallyзакончится, и только потом уходит вызывающему коду. - Свой класс исключения наследует
Exceptionили его потомка; сообщение и код он получает от родителя. catchпо родительскому классу ловит и потомков.get_class($e)называет настоящий класс.throw $eпередаёт исключение дальше.