Где и как лучше прехватывать/обрабатывать исключения?

Хочу разобраться, как и где лучше/правильней обрабатывать исключения и что отдавать пользователю.

Есть код, он должен сохранить пользователя. В этом примере мы можем получить Exception в 3 разных местах.

Как я сейчас представляю, обернуть в контроллере вызов метода сервиса в try catch. Поскольку в обязанность контроллера входит генерирование ответа пользователю в зависимости от результата. Но, меня смущают некоторые нюансы:

  • В случаи большого количества возможных исключений методы контроллера превратятся в простыню из catch.
    Еще и ошибку нужно записать в лог.
  • Если обрабатывать ошибку по месту, возникает проблема в возврате обратно данных по цепочке. Это наплодит кучу if-ов и усложнит разные методы разными типами возврата.


Если поместить try catch в сервис и возвращать контроллеру к примеру User|false, как тогда контроллер будет генерировать ответ, если он не будет знать в чем проблема, в базе или в том, что это дубликат.
Может лучшим способом будет обрабатывать все ошибки через set_exception_handler?
class UserController
{
    private UserSerivce $service;

    public function store(array $data)
    {
        $user = $this->service->new($data);

        return redirect()->to('/' . $user->id);
    }
}

class UserSerivce
{
    private UserRepository $repository;

    public function new(array $data): User
    {
        $user = new User($data['name']); //InvalidArgumentException

        $user = $this->repository->add($user);

        return $user;
    }
}

class UserRepository
{
    public function __construct()
    {
        $this->builder = new PDO(''); //PDOException
    }
    public function add(User $user): User
    {
        $saved = $this->builder->query('INSERT INTO users (name) VALUES ("user")'); //PDOException
    }
}

class User
{
    private string $name;

    public function __construct(string $name)
    {
        if (empty($name)) {
            throw new InvalidArgumentException('Name is empty');
        }
        $this->name = $name;
    }
}
  • Вопрос задан
  • 547 просмотров
Решения вопроса 1
FanatPHP
@FanatPHP
Чебуратор тега РНР
Главная вещь, которую надо понимать про исключения, это то что они бывают двух основных видов.
После этого вся обработка становится совершенно естественной и очевидной.

- Error exceptions, или по простому говоря - ошибки. Обычные ошибки при выполнении программы. Обычно код бросает их сам. Решение "обрабатывать все ошибки через set_exception_handler" будет вполне логичным.
- Business logic exceptions - это не ошибка в строгом понимании этого слова, а скорее нормальное поведение программы. Ситуация исключительная, но только для бизнес-логики. Их всегда кидает программист.

И вот просто тупо исходя из того что их два типа, уже можно сказать что единого ответа "где лучше" не существует.
У каждого типа своя логика обработки. Но при этом, как только ты уложил в голове различия, эта логика становится совершенно очевидной:

- Error exceptions почти никогда не ловятся через try-catch, по крайней мере на месте. За исключением редких исключительных ситуаций обработка ошибок производится в единой точке, обработчике ошибок
- Business logic exceptions всегда ловятся через try-catch

Отсюда мы видим, что

- //PDOException при коннекте (эммм... я понимаю что пример, но блин, new PDO в конструкторе репы, серьёзно?ладно, мы сейчас не об этом) - это однозначно ошибка
- //PDOException в запросе - это тоже ошибка, тут два раза думать не надо
- условно пустое имя. Ну вот здесь мы уже переходим в область бизнес-логики. Коду тут без разницы, пустое имя, или полное. Это важно нам - программисту, пользователю.

Но тут есть один, блин, тонкий момент.
Валидация, по сути, пытается разорваться между всеми слоями приложения.
С одной стороны, это функция Сущности (которую ошибочно называют моделью) - проверять валидность своих данных.
С другой - если нам надо донести результаты валидации до пользователя, то как быть с переводами? Тащить в модель переводчик, серьёзно? Ну ок, ладно, возвращаем ключи для перевода. Хотя тоже как-то...
Но вот проверка емейла на уникальность. Её-то где делать?
В Сущности? И тащить в нее соединение с БД?
На уровне БД? А где ловить тогда исключение? В сервисе? И ломиться через несколько уровней абстракции к сырому PDOException? Не вариант.
Или, к примеру, для модели естественно проверять каждое поле отдельно, и кидать исключение. А для пользовательского интерфейса это неприемлемо - надо выдавать все ошибки валидации скопом, а не скармливать по одной.
Вопросы...

Но "где валидировать данные" - это отдельная тема, которая не относится напрямую к вопросу "где ловить ошибки".

В данном случае я предлагаю оставить Сущность Юзер без валидации, а всю валидацию делать в сервисе.

Хотя опять же - в современных фреймворках валидацию (Не будем показывать пальцем, но это был Ларавель) вообще делают еще до запуска контроллера, в миддлвари. Это кстати спорное решение, которое нарушает целостность модели. Если мы обращаемся к модели через другую точку входа, не контроллер, а, к примеру, создаем юзера через командную строку, то нам нужна точно такая же валидация. Запускать команды через мидлварь? В сущности, это мысль... Но всё равно, мы в итоге бизнес-логику размазываем между моделью и точками входа в неё, а это костыль.
И при всём при этом оставлять Сущность Юзер совсем без валидации тоже как-то не комильфо... А если оставлять - то получится по сути дублирование кода.
Вопросы, вопросы...

Но вернемся к нашим баранам, в смысле юзерам.

Начнем с того, что проверка имени на равенство пустой строке или нулю - это какой-то детский лепет (и кстати, почему ноль нельзя? вот у Маска ребеночка зовут X Æ A-12 - почему у кого-то не может быть имя "0.0"?).

Отдельно побурчу насчет empty. Вообще, это один из самых сложных операторов, на нем спотыкаются все поголовно. В частности,
function f($name){
    if (empty($name))...

- это бессмыслица. Звучит, в переводе на русский, немного шизофренически: "пусть у нас будет переменная $name. Если у нас нет переменной $name...". Ну как нет, если мы только что ее в функцию передали?
empty() проверяет переменную на существование И "пустоту". И в данном случае первая проверка будет бессмысленной. Никогда не надо писать бессмысленный код.
Поэтому логичнее будет написать просто if(!$name). Хотя по нынешним временам это тоже говнокод. Что мы имеем здесь в виду? Имя не может быть пустой строкой? Пустым массивом? Нулём? null? false? А true или заполненный массив - это, получается, хорошее, годное имя?
Лучше все-таки четче определять свои претензии. К примеру, проверять длину строки.

У имени можно сделать миллион проверок: Минимальную длину, максимальную длину. Проверить что это строка, а не массив, в конце концов, и не булево значение.
И так по каждому полю.
То есть, по-хорошему, валидация данных на соответствие правилам бизнес-логики - это отдельный большой бизнес!

При этом, по итогам валидации, исключение кидается строго одно, общее для всех ошибок.
Которое мы и ловим в контроллере через трай.

class ValidationError extends Exception{ ... };
 
class User
{
    private string $name;

    public function __construct(string $name)
    {
        $this->name = $name;
    }
}

class UserRepository
{
    public function __construct(PDO $pdo)
    {
        $this->builder = $pdo;
    }
    public function add(User $user): User
    {
        $saved = $this->builder->query('INSERT INTO users (name) VALUES ("user")'); //PDOException
    }
}

class UserSerivce
{
    private UserRepository $repository;
    private Validator $validator;

    public function new(array $data): User
    {
        $rules = [...] ;
        $errors = $this->validator->validate($data, $rules);
        if ($errors) {
            throw new ValidationException("", $errors);
        }
        $user = new User($data['name']); // в принципе, сучность может здесь бросить своё ValidationException
        return $this->repository->add($user);
    }
}

class UserController
{
    private UserSerivce $service;

    public function store(array $data)
    {
        try {
            $user = $this->service->new($data);
        catch (ValidationError $e) {
            // рассказываем юзеру что он дурак
        }
        return redirect()->to('/' . $user->id);
    }
}


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

Как можно заметить, вот весь этот длинный и путанный текст посвящен исключительно ошибкам бизнес-логики.
Поскольку с ошибками кода всё куда проще - единый хендлер тупо их обрабатывает в одном месте, как описано в статье по ссылке @Spartak-2205
За исключением редких случаев, когда они ловятся по месту. Когда ошибка некритичная, или есть сценарий обработки - например, попробовать выполнить то же действие еще раз.
Ответ написан
Пригласить эксперта
Ответы на вопрос 1
Spartak-2205
@Spartak-2205
Разработка и создание сайтов
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы