Открой безлимитный доступ ко всем скринкастам и получай большие скидки на все наши мероприятия:
Бесплатный
0/ мес.

Ограниченный доступ
только к Free-видео

Активен
Подписчик
500/ мес.

Безлимитный доступ
ко всем скринкастам

Принимаем оплату российскими и иностранными картами, системами МИР Pay, Яндекс Pay и Tinkoff Pay.

Исключения и контроль ошибок

Подходы к контролю исключительных ситуаций. Использование исключений и корректный ох отлов.

Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Комментарии (38)
voodooism

Было бы хорошо если бы к видео отдельно прикладывались слайды

Ответить
Александр

значит ли что в MVC в большинстве случаях, trow (выброс) создается в моделях, отлавливается в контроллере, контроллер формирует нужную строку или массив или объект, и передает в вид, ну или возвращает аякс запросу объект, для последующего вывода в удобном формате на фронте ?

А если в самом контроллере(или там в скрипте для процедурного стиля) создавать trow , то как правильно его в этом же контроллере обработать через try-catch , и вобще правильный ли такой подход все в одном файле ?

Ответить
Дмитрий Елисеев

В общем случае throw кидается всегда чем-то внутренним, а вверху либо отлавливается в контроллере или посреднике (чтобы сформировать красивый ответ с ошибкой), либо перехватывается самим фреймворком и пишется в логи, выводя ответ 500 Server Error.

Ответить
Александр

Дмитрий, спасибо вам! Если вас не затруднит, не могли бы ли вы рассказать про логирование?

Ответить
Артем

Понравилось "выкинуть на улицу")

Ответить
S.Polessky

У меня было такое один раз.

Ответить
Ruslan

Кадр 44:44 акшен в контролере . Мы так во флэш выбросим сообщение. А что будет с рендером? - Вывод об ошибке на всю страницу и потеря всех заполненных данных? Чтоб этого не было должны ли мы передать наш шаблон с переменной $form ?

Ответить
Дмитрий Елисеев

После флэша экшен не прерывается и выводится тот же $this->render со всеми данными формы. И над формой выводим блок с флэш-сообщением.

Ответить
Ruslan

Спасибо. Видно затмение в мозгу, решил, что после catch код исполняться не будет.

Ответить
Джамал

Здравствуйте, такой вопрос. Часто в API проектах наблюдаю такой класс ClientException и в нем есть метод render в котором написан код который отдает ошибку в виде json. Является ли это хорошей практикой ? Если нет, то лучше ловить ClientException и в блоке catch отдавать ответ в виде json?

Ответить
Дмитрий Елисеев

Это не всегда удобно.

Если нужно выводить ошибку как-нибудь по-своему, то тогда встроенный метод render не подойдёт.

Ответить
Джамал

Спасибо!

Ответить
S.Polessky

Есть ли правила, когда нужно создавать свой уникальный класс Exception, а не использовать встроенные вроде DomainException?

Всегда грызли сомнения: с одной стороны, желательно чтобы какой-то сервис имел свой класс Исключения, но и плодить сущности не хочется чтобы в них не потеряться.

Ответить
S.Polessky

Не обнаружив конкретного набора правил, стал более осознанно к исключениям подходить.

Сегодня натолкнулся на ситуацию, в которой вшитое исключение необходимо преобразовать в кастомное.

Было исключение:

throw new \DomainException("Недостаточно средств на балансе.")

Появилась бизнес-задача - показать пользователю, что он может пополнить баланс. Можно дополнить исключение так:

throw new \DomainException("Недостаточно средств на балансе. " . Html::a("Пополнить баланс?", ['/deposit/add']));

..но это ухудшает читабельность, и создаёт избыточность из-за добавление класса Html:

Поэтому, вводим кастомное:

throw new NotEnoughBalanceException("Недостаточно средств на балансе.")

...а в Controller'е можем уже добавить ссылку на пополнение баланса

Ответить
Дмитрий Елисеев

В идеале лучше делать отдельные классы для каждого исключения. Но на практике это имеет смысл:

Когда пишем библиотеку, которую будем выкладывать публично. Там желательно сделать общий базовый класс исключения для всей библиотеки и кастомные классы для каждого случая. Тогда пользователь нашей библиотеки сможет по своему желанию ловить либо базовое исключение, либо конкретные.

Когда в нашем коде разные ошибки нужно обрабатывать по-разному в разных catch. Например, если метод может выбрасывать NotEnoughBalanceException и NotConfirmedPhoneException, по которым нам нужно выше делать разные вещи.

Когда исключение может содержать дополнительные поля или методы для формирования сообщения или логирования вроде new NotEnoughException($balance) или NotEnoughException::fromBalance($balance) с последующим использованием выше метода $exception->getBalance().

Ответить
S.Polessky

"RuntimeException - для инфраструктурных проблем "

А можно несколько примеров таких проблем? Если, допустим, нужен расширение intl, а его нет - это RuntimeException, так?

Ответить
Дмитрий Елисеев

Да, проблемы отсутствия расширений, ошибок сети, подключения к БД,

Ответить
S.Polessky

Вот и нашел пример для LogicException. Не знаю корректен ли он, надеюсь Дмитрий даст свою оценку.

Кейс: В магазине была возможность заказа только без регистрации. Ввели регистрацию, за ней функцию - привязать свои гостевые заказы по e-mail.

public function assignUser($userId)
{
    if ($this->user_id) {
        throw new \LogicException("User already assigned");
    }

    $this->user_id = $userId;
}

Почему LogicException: Документация нам гласит: "Исключение, которое представляет ошибку в логике программы. Такой тип исключений должен непосредственно привести к исправлениям в вашем коде". В коде больше не осталось мест, где пользователь мог бы напрямую "дергнуть" привязку - например, указав orderID в форме.

Значит ошибка может быть только в коде, например - мы забыли в QueryBuilder прописать условие не получать уже привязанные заказы:

Order::find()->where(['email' => $email])->all()

// Выведет все заказы, в т.ч. уже привязанные. Для исправления добавить: ->andWhere(['user_id' => null])

P.S. Ошибся веткой комментариев. Этот комментарий для комментария ниже :)

Ответить
Дмитрий Елисеев

Для 'User already assigned' мы у себя чаще используем DomainException, так как это ошибка бизнес-логики, а не просто логики.

Ответить
S.Polessky

Поделюсь своим кейсом, где применил это исключение:

PayPal стал плохо обрабатывать верификацию запросов: вместо VERIFIED, INVALID порой стал возвращать "Fatal Failure". Повторные попытки - решали проблему.

Но т.к. время выполнения PHP ограничено, а число рекурсий неизвестно - ввёл лимит. А для исключения - RuntimeException.

Сниппет:

public function validate(array $data, $retryCount = 0)
{
    $result = $this->sendDataToValidator($data);

    if (($result === "VERIFIED" || $result === "INVALID")) {
        return $result === "VERIFIED";
    }

    if ($retryCount === self::RETRY_QUERY_QUANTITY) {
        throw new \RuntimeException("Can not resolve PayPal. Reached limit of retries");
    }

    return $this->validate($data, ++$retryCount);
}

P.S. Осталось найти примеры для LogicException :)

Ответить
Владимир Перепеченко

Дмитрий, как всегда, интересно/полезно.

Жаль что не раскрыта тема PHP ошибок (Errors, а не Exceptions). Они (Errors) всегда есть и их надо логгировать и выводить сообщения на критичных. Но есть ньюансы:

  • какие то ошибки ошибки прерывают скрипт, какие то нет
  • если по каждой ошибке бросать ErrorExcepttion который зачем-то существует в PHP, то вызванное исключение всегда прервёт исполнение программы
  • в примерах обработок Exceptions в Ваших скринкастах нет примеров обработки Errors к миддлеверах (того же Slim). Только обработка исключений.

Для меня это дело остаётся ещё темным, особенно - как обрабатывать Errors в Slim middleware. Просьба хотя бы намекнуть.

Ответить
Владимир Перепеченко

Дело осложняется тем, что некоторые Error при крахе не выкидываются и чтобы их залоггировать приходится вытаскивать их через функции:

register_shutdown_function()
error_get_last()

или это всё надуманные проблемы у меня в голове?

Ответить
Владимир Перепеченко

Честно говоря, я плохо понимаю связь между PHP ошибками, которые я пытаюсь отловить через set_error_handler(), и объектом Error https://www.php.net/manual/ru/class.error.php

Возможно, что этой связи нет вовсе. Тут теряюсь, ибо моя задача логгировать все ошибки PHP + исключения, не отловленные через try/catch. При этом, чтобы безобидные ошибки (notice, warning) только логгировать, а не бросать для них ErrorException, который прервет работу программы.

Как реализовать это надежно - пока непонятно. Подкинете идей, Дмитрий?

Ответить
Владимир Перепеченко

Чуть прояснилось в голове после прочтения Ошибки в PHP 7

Но все равно есть туман: - остались ли в PHP 8 PHP ошибки или они все уже заменены на Errors imlements Throuble - если остались, то какие из них фатальные, что прерывают программу и не отлавливаются через set_error_handler() и как их всё таки залоггировать

Ответить
Дмитрий Елисеев

Постепенно в PHP многие старые ошибки переделывают на отлавливаемые по Throwable исключения. Так что прогресс в этом есть.

Ответить
Дмитрий Елисеев

При этом, чтобы безобидные ошибки (notice, warning) только логгировать, а не бросать для них ErrorException, который прервет работу программы.

Я предпочитаю максимальную строгость, когда нет ничего безобидного и когда абсолютно всё прерывает программу.

Ответить
Владимир Перепеченко

>когда абсолютно всё прерывает программу.

то есть рекомендуете останавливать продакшен из за: E_WARNING E_DEPRECATED E_NOTICE

у меня куча legacy лапшекода - продакшен сразу ляжет :)

Ответить
Дмитрий Елисеев

Рекомендую всё это включать в dev окружении и сразу исправлять в коде, чтобы в продакшене этого никогда не было.

В легаси тоже постепенно исправить. Много недочётов и все deprecated находит Psalm.

Ответить
Владимир Перепеченко

Понял, thanks!

Ответить
Дмитрий Елисеев

моя задача логгировать все ошибки PHP + исключения, не отловленные через try/catch

Как-то отдельно их ловить и логировать необязательно. Если что-то фатальное не поймалось блоком try-catch в приложении, то это залогирует у себя сам PHP-FPM. Просто смотрите и его логи.

Ответить
Владимир Перепеченко

Благодарю за пояснения, Дмитрий. Стало больше определённости.

Ответить
Tema

Спасибо Дмитрий становится более понятно.

Есть впечатление что DTO появилось из за рефакторинга если в функции или методе более 2 параметров и если они логически связаны например user то их можно вынести в interface, class и использовать вместо 3 и более параметров

class CreateUserDto
{
    public string $name;
    public string $email;
    public string $password;
}

class UserController {
public function create($createUserDto CreateUserDto) {
    return $createUserDto;
}

$user = new UserController();
$user->create('name', 'email', 'pass');

@Controller('users')
export class UsersController {
  public constructor(private readonly usersService: UsersService) {}

  @Post()
  public async create(@Body() createUserDto: CreateUserDto) {
    return await this.usersService.create(createUserDto);
  }

import { IsEmail, IsNotEmpty } from 'class-validator';

export class CreateUserDto{
 @IsNotEmpty()
  name: string;

  @IsEmail()
  email: string;

  @IsNotEmpty()
  password: string;
}
Ответить
Дмитрий Елисеев

Да, как один из вариантов. Вместо отдельных аргументов удобно передавать структуры с несколькими значениями.

Ответить
Tema

Дмитрий если у usera email неверный то какую ошибку лучше выбрать? BadRequestException?

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

Не совсем ещё понял что можно делать внутри сервиса( Вроде удобно сервис внутри сервиса использовать я не знаю плохая это практика или нет( И не слишком ли избыточно делать проверку и хешировать пароль так?

import { BadRequestException, Injectable } from '@nestjs/common';
import { UsersService } from './../users/users.service';
import { CreateAuthDto } from './dto/create-auth.dto';
import { UpdateAuthDto } from './dto/update-auth.dto';
import * as bcrypt from 'bcrypt';
import { User } from '@prisma/client';

@Injectable()
export class AuthService {
  private user: User;

  public constructor(private readonly usersService: UsersService) {}

  public async register(
    createAuthDto: CreateAuthDto,
  ): Promise<User | BadRequestException> {
    this.user = await this.usersService.findOne({ email: createAuthDto.email });

    if (this.user && this.user.email === createAuthDto.email) {
      throw new BadRequestException('Choose another email');
    }

    createAuthDto.password = await bcrypt.hash(createAuthDto.password, 12);

    return await this.usersService.create(createAuthDto);
  }
}
{
    "statusCode": 400,
    "message": "Choose another email",
    "error": "Bad Request"
}
Ответить
Дмитрий Елисеев

Дмитрий если у usera email неверный то какую ошибку лучше выбрать? BadRequestException?

У меня ошибки валидации возвращаются с HTTP-статусом 422 UnprocessableEntity, а внутри сервиса и сущности доменные ошибки DomainException перехватываются и преобразуются в статус 409 Conflict.

Ответить
Дмитрий Елисеев

Вроде удобно сервис внутри сервиса использовать я не знаю плохая это практика или нет(

Сервисы уровня приложения могут спокойно вызывать доменные сервисы и репозитории.

И не слишком ли избыточно делать проверку и хешировать пароль так?

Похожее мы делаем в эпизоде о регистрации.

Ответить
Tema

Спасибо вам большое Дмитрий

Ответить
Юрий

Просьба объяснить что означают фразы "доменные сервисы" или "доменная логика". Никак не могу понять для себя что именно это значит. Вроде понимаю но на интуитивном уровне все очень туманно.

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

"Сервисы уровня приложения могут спокойно вызывать доменные сервисы"

Где эта граница, раз мы отделяем приложения от домена???

PS Я пытался нагуглить, но объяснения на вики довольно размытые. Из-за чего чувствую себя глупо так как не могу уловить ту суть, которую все вкладывают в это понятия "домена".

Прошу объяснить на простых примерах.

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий

Или войти через:

Yandex
MailRu
GitHub
Google