Принимаем оплату российскими и иностранными картами, системами МИР Pay, Яндекс Pay и Tinkoff Pay.
Исключения и контроль ошибок
Подходы к контролю исключительных ситуаций. Использование исключений и корректный ох отлов.
Скрытый контент (код, слайды, ...) для подписчиков. Открыть →
Дмитрий Елисеев
elisdn.ru
Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram
Комментарии (39)
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 при крахе не выкидываются и чтобы их залоггировать приходится вытаскивать их через функции:
Возможно, что этой связи нет вовсе. Тут теряюсь, ибо моя задача логгировать все ошибки 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);
}
}
Дмитрий если у usera email неверный то какую ошибку лучше выбрать? BadRequestException?
У меня ошибки валидации возвращаются с HTTP-статусом 422 UnprocessableEntity, а внутри сервиса и сущности доменные ошибки DomainException перехватываются и преобразуются в статус 409 Conflict.
Дмитрий Елисеев
Вроде удобно сервис внутри сервиса использовать я не знаю плохая это практика или нет(
Сервисы уровня приложения могут спокойно вызывать доменные сервисы и репозитории.
И не слишком ли избыточно делать проверку и хешировать пароль так?
Просьба объяснить что означают фразы "доменные сервисы" или "доменная логика".
Никак не могу понять для себя что именно это значит. Вроде понимаю но на интуитивном уровне все очень туманно.
В течении всего скринкаста я думал о доменной логике, как о тем классах и сущностях, которые я порождаю во время создания программы. Но после комментария выше я понял что видимо ошибался.
"Сервисы уровня приложения могут спокойно вызывать доменные сервисы"
Где эта граница, раз мы отделяем приложения от домена???
PS Я пытался нагуглить, но объяснения на вики довольно размытые. Из-за чего чувствую себя глупо так как не могу уловить ту суть, которую все вкладывают в это понятия "домена".
Было бы хорошо если бы к видео отдельно прикладывались слайды
значит ли что в MVC в большинстве случаях, trow (выброс) создается в моделях, отлавливается в контроллере, контроллер формирует нужную строку или массив или объект, и передает в вид, ну или возвращает аякс запросу объект, для последующего вывода в удобном формате на фронте ?
А если в самом контроллере(или там в скрипте для процедурного стиля) создавать trow , то как правильно его в этом же контроллере обработать через try-catch , и вобще правильный ли такой подход все в одном файле ?
В общем случае throw кидается всегда чем-то внутренним, а вверху либо отлавливается в контроллере или посреднике (чтобы сформировать красивый ответ с ошибкой), либо перехватывается самим фреймворком и пишется в логи, выводя ответ 500 Server Error.
Дмитрий, спасибо вам! Если вас не затруднит, не могли бы ли вы рассказать про логирование?
Понравилось "выкинуть на улицу")
У меня было такое один раз.
Кадр 44:44 акшен в контролере . Мы так во флэш выбросим сообщение. А что будет с рендером? - Вывод об ошибке на всю страницу и потеря всех заполненных данных? Чтоб этого не было должны ли мы передать наш шаблон с переменной $form ?
После флэша экшен не прерывается и выводится тот же
$this->render
со всеми данными формы. И над формой выводим блок с флэш-сообщением.Спасибо. Видно затмение в мозгу, решил, что после catch код исполняться не будет.
Здравствуйте, такой вопрос. Часто в API проектах наблюдаю такой класс ClientException и в нем есть метод render в котором написан код который отдает ошибку в виде json. Является ли это хорошей практикой ? Если нет, то лучше ловить ClientException и в блоке catch отдавать ответ в виде json?
Это не всегда удобно.
Если нужно выводить ошибку как-нибудь по-своему, то тогда встроенный метод render не подойдёт.
Спасибо!
Есть ли правила, когда нужно создавать свой уникальный класс Exception, а не использовать встроенные вроде DomainException?
Всегда грызли сомнения: с одной стороны, желательно чтобы какой-то сервис имел свой класс Исключения, но и плодить сущности не хочется чтобы в них не потеряться.
Не обнаружив конкретного набора правил, стал более осознанно к исключениям подходить.
Сегодня натолкнулся на ситуацию, в которой вшитое исключение необходимо преобразовать в кастомное.
Было исключение:
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()
."RuntimeException - для инфраструктурных проблем "
А можно несколько примеров таких проблем? Если, допустим, нужен расширение intl, а его нет - это RuntimeException, так?
Да, проблемы отсутствия расширений, ошибок сети, подключения к БД,
Вот и нашел пример для
LogicException
. Не знаю корректен ли он, надеюсь Дмитрий даст свою оценку.Кейс: В магазине была возможность заказа только без регистрации. Ввели регистрацию, за ней функцию - привязать свои гостевые заказы по e-mail.
Почему LogicException: Документация нам гласит: "Исключение, которое представляет ошибку в логике программы. Такой тип исключений должен непосредственно привести к исправлениям в вашем коде". В коде больше не осталось мест, где пользователь мог бы напрямую "дергнуть" привязку - например, указав orderID в форме.
Значит ошибка может быть только в коде, например - мы забыли в QueryBuilder прописать условие не получать уже привязанные заказы:
// Выведет все заказы, в т.ч. уже привязанные. Для исправления добавить: ->andWhere(['user_id' => null])
P.S. Ошибся веткой комментариев. Этот комментарий для комментария ниже :)
Для
'User already assigned'
мы у себя чаще используемDomainException
, так как это ошибка бизнес-логики, а не просто логики.Поделюсь своим кейсом, где применил это исключение:
PayPal стал плохо обрабатывать верификацию запросов: вместо
VERIFIED
,INVALID
порой стал возвращать "Fatal Failure". Повторные попытки - решали проблему.Но т.к. время выполнения PHP ограничено, а число рекурсий неизвестно - ввёл лимит. А для исключения -
RuntimeException
.Сниппет:
P.S. Осталось найти примеры для
LogicException
:)Дмитрий, как всегда, интересно/полезно.
Жаль что не раскрыта тема PHP ошибок (Errors, а не Exceptions). Они (Errors) всегда есть и их надо логгировать и выводить сообщения на критичных. Но есть ньюансы:
Для меня это дело остаётся ещё темным, особенно - как обрабатывать Errors в Slim middleware. Просьба хотя бы намекнуть.
Дело осложняется тем, что некоторые Error при крахе не выкидываются и чтобы их залоггировать приходится вытаскивать их через функции:
или это всё надуманные проблемы у меня в голове?
Честно говоря, я плохо понимаю связь между 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 исключения. Так что прогресс в этом есть.
Я предпочитаю максимальную строгость, когда нет ничего безобидного и когда абсолютно всё прерывает программу.
>когда абсолютно всё прерывает программу.
то есть рекомендуете останавливать продакшен из за: E_WARNING E_DEPRECATED E_NOTICE
у меня куча legacy лапшекода - продакшен сразу ляжет :)
Рекомендую всё это включать в
dev
окружении и сразу исправлять в коде, чтобы в продакшене этого никогда не было.В легаси тоже постепенно исправить. Много недочётов и все deprecated находит Psalm.
Понял, thanks!
Как-то отдельно их ловить и логировать необязательно. Если что-то фатальное не поймалось блоком try-catch в приложении, то это залогирует у себя сам PHP-FPM. Просто смотрите и его логи.
Благодарю за пояснения, Дмитрий. Стало больше определённости.
Спасибо Дмитрий становится более понятно.
Есть впечатление что DTO появилось из за рефакторинга если в функции или методе более 2 параметров и если они логически связаны например user то их можно вынести в interface, class и использовать вместо 3 и более параметров
Да, как один из вариантов. Вместо отдельных аргументов удобно передавать структуры с несколькими значениями.
Дмитрий если у usera email неверный то какую ошибку лучше выбрать? BadRequestException?
Не совсем ещё понял что можно делать внутри сервиса( Вроде удобно сервис внутри сервиса использовать я не знаю плохая это практика или нет( И не слишком ли избыточно делать проверку и хешировать пароль так?
У меня ошибки валидации возвращаются с HTTP-статусом 422 UnprocessableEntity, а внутри сервиса и сущности доменные ошибки DomainException перехватываются и преобразуются в статус 409 Conflict.
Сервисы уровня приложения могут спокойно вызывать доменные сервисы и репозитории.
Похожее мы делаем в эпизоде о регистрации.
Спасибо вам большое Дмитрий
Просьба объяснить что означают фразы "доменные сервисы" или "доменная логика". Никак не могу понять для себя что именно это значит. Вроде понимаю но на интуитивном уровне все очень туманно.
В течении всего скринкаста я думал о доменной логике, как о тем классах и сущностях, которые я порождаю во время создания программы. Но после комментария выше я понял что видимо ошибался.
"Сервисы уровня приложения могут спокойно вызывать доменные сервисы"
Где эта граница, раз мы отделяем приложения от домена???
PS Я пытался нагуглить, но объяснения на вики довольно размытые. Из-за чего чувствую себя глупо так как не могу уловить ту суть, которую все вкладывают в это понятия "домена".
Прошу объяснить на простых примерах.
Это подробно показываем в отдельной серии по разработке проекта c 11-го эпизода.
Или войти через: