Как работает поимка исключений catch?

class ValidData {
    public $email;
    public $url;
    function validEmail($email) {
        $this->email = $email;
        
        if(!filter_var($this->email, FILTER_VALIDATE_EMAIL)) throw new Exception("Email неверный.", 1);
    }

$newdata = new ValidData;
try {
    $newdata->validEmail('[email protected]');
} catch (Exception $e) {
    echo 'Выброшено исключение: ' .  $e->getMessage() . ' Код исключения: ' . $e->getCode() . "\n";
}

Вопрос наверное совсем глупый, но подскажите, как работает $e в catch (Exception $e). В throw new Exception передано 2 параметра. Каким образом из одной переменной $e вытаскиваются оба эти параметра?


Ответы (2 шт):

Автор решения: Алексей Шиманский

Исключения - это специальные классы со своими свойствами и методами. Даже не зря при выбросе исключения пишется new.

Соответственно, при вызове исключения, мы по сути передаём аргументы в конструктор класса, а при работе с исключениями работаем с методами этого класса. Вот и всё ¯\_(ツ)_/¯

→ Ссылка
Автор решения: Ипатьев

Как работает catch

Если отвечать на вопрос из заголовка, то поимка исключений catch работает очень просто:

В блоке catch мы пишем тип исключения, которое хотим поймать. При этом будут также пойманы все исключения, которые унаследованы от этого типа.

Поскольку исключения сами по себе являются классами, и могут наследоваться друг от друга, то у нас получается иерархия исключений: от интерфейса Throwable наследуются два класса, Error и Exception, а остальные исключения - от этих классов.

Соответственно, сatch может ловить только определенные исключения из этой иерархии. Причем ловится как само указанное исключение, так и все его потомки. То есть если ловить Throwable, то будут пойманы вообще все исключения. Если ловить ValueError, то будут пойманы исключения только этого класса. Если ловить ArithmeticError, то будут пойманы как исключения этого класса, так и DivisionByZeroError.

При этом блоков catch может быть несколько. То есть можно по-разному обрабатывать разные уровни исключений

try {
    // какой-то код
} catch (TypeError $e) {
    // обрабатываем только ошибку типа
} catch (Throwable) {
    // обрабатываем все остальные исключения
}

При этом надо помнить, что исключения может ловить не только catch, но и глобальный обработчик исключений. Который в любом случае должен быть в любом серьёзном приложении. Что означает, что большую часть исключений специально ловить вообще не нужно. В частности, исключение никогда не нужно ловить, чтобы проинформировать программиста об ошибке. РНР прекрасно справится этим сам, без вашей помощи.

Как НЕ ДОЛЖЕН работать catch

Если же посмотреть на код, приведенный в вопросе, то станет видна ещё одна проблема: В коде новичков поимка исключений чаще всего работает неправильно.

Что мы и видим на примере этого кода. Понятно, что он учебный, из какого-нибудь курса. Но в том-то и проблема, что автор курса не осилил нормальный пример, и в итоге учит делать все неправильно. Получая наглядное пособие о том, как делать не надо.

  • во-первых, и в самых главных: у новичков throw всегда, обязательно, в 100% случаев заключается в catch. Хотя на самом деле это совершенно не связанные между собой конструкции. В реальном коде catch либо находится сильно дальше от throw, либо отсутствует вообще, а исключение обрабатывается глобальным обработчиком. Если же исключение ловить вот прям сразу, то становится непонятно, зачем оно вообще было нужно: ведь можно просто проверить результат выполнения функции. validEmail() вполне может возвращать boolean и вместо громоздкого try-catch можно написать простой if

      if ($newdata->validEmail('[email protected]')) {
          echo "Неверный email";
      }
    

    а идея исключений как раз в том, что они чаще всего ловятся где-то ещё, а не там же, где выброшены.

  • во-вторых, исключение здесь кидает функция validateEmail. Что совершенно неправильно. Исключение функция должна кидать, когда не может выполнить свою работу. А эта функция отработала совершенно нормально, определив, является ли емейл корректным. Исключение в данном случае может кидать конструктор. Который с помощью функции validateEmail проверит корректность адреса, и если он не проходит проверку - то выбросить исключение. Поскольку конструктор не сможет выполнить свою работу - создать объект класса из-за некорректных данных

  • во-третьих, исключение кидается самого общего типа, Exception. А должно кидаться исключение специального типа, который надо заранее создать, например ValidationException, чтобы потом можно было ловить только его, и не обрабатывать какие-то другие, посторонние исключения.

  • и в-четвертых, в блоке catch мы тут просто выводим текст исключения и его код, что является и вовсе глупостью: если полностью убрать всю эту сбрую с try, catch, и echo, то ничего не изменится - РНР и так сам по себе выведет ту же самую информацию. Блок catch надо использовать только тогда, когда требуется какое-то более осмысленное действие, чем просто вывести ошибку. Чаще всего, ничего выводить вообще не надо, но если уж и выводить, то как-то дополнив сообщение об ошибке.

Чтобы сделать этот пример более осмысленным,

class ValidationException extends ValueError{};

class ValidData {
    public $email;
    public function __construct($email) {
        if (!$this->validEmail($email)) {
             throw new ValidationException("Неверный email");
        }
        $this->email = $email;
    }
    protected function validEmail($email) {
        return filter_var($this->email, FILTER_VALIDATE_EMAIL);
    }
}

try {
    $newdata = new ValidData('invalid');
} catch (ValidationException $e) {
    throw new RuntimeError("Невозможно создать объект из-за некорректных данных", 0, $e);
}

Здесь мы вместо вывода сообщения на экран (что практически никогда не делается в реальных приложениях), выбрасываем исключение другого типа, которое будет обработано где-то ещё.

"Как вытаскиваются оба параметра"

Ну а на "совсем глупый" вопрос уже ответили. $e - это объект класса, унаследованного от Throwable. Точно так же, как $newdata - это объект класса ValidData. То есть у него есть конструктор, есть свойства, есть методы. При создании объекта здесь в конструктор передаются два параметра, значения которых назначаются свойствам класса и могут быть выведены через соответствующие методы. Чтобы не возникало таких вопросов, надо почитать про основы объектов в РНР

→ Ссылка