Exceptions

Adelf

Administrator
Команда форума
Домен не отправляет почту.
Домен генерит какой-нибудь эвент OrderCreated.
А уже приложение реагирует на него отправкой почты.
 

WMix

герр M:)ller
Партнер клуба
Возвращать статус из команды, тоже не самое правильное решение. Это подразумевает дополнительную проверку этого статуса хотябы на наличие положительного ответа.

Возвращать статус
статус ответа команды
проверку на что?
на наличие положительного ответа

дополнительную к чему?
к вызову команды

если мы обсуждаем проверку ответа API клиентом
нет, создание этого ответа, для начала

в этом
допустим, статус заказа: cancelled, приходит запрос UPDATE /orders/123 {"status":"cancelled"}, логичным ответом будет 200 OK
если в запросе будет UPDATE /orders/123 {"status":"processing"} - ответ будет 403 Forbidden
в моих постах нет упоминаний ни о проверках, ни про обработчики - ни единого
правильно, потому как ты умалчиваешь условие когда ты возвращаешь http 400

но по твоим рассуждениям это выгдядит так
Код:
$result = $orderService->cancel(...);
if($result->status !== 'cancelled'){
   http_response_code(400);
}

не брызгай слюной, плиз
 

Adelf

Administrator
Команда форума
правильно, потому как ты умалчиваешь условие когда ты возвращаешь http 400
мне даже больше интересно, почему у него 403 возвращается в одном из моментов. У него домен знает про http коды? или такая продвинутая авторизация... что вообще все тогда можно 403 ошибкой возвращать. а не 400. По моему 403 когда пытаются сделать что-то сделать с отмененным заказом... вроде и правильно, но это пример когда авторизация сильно пролезла в домен. не самый лучший вариант.
 

WMix

герр M:)ller
Партнер клуба
почему у него 403
он о REST вероятно, ресурс: order-123 запрещен.. на самом деле не суть, разговор то больше про 4xx код
По моему 403 когда пытаются сделать что-то сделать с отмененным заказом
у нас чаще 403 появляется когда вызывают action на который нет прав.
 

Adelf

Administrator
Команда форума
Да. Теоретически можно себе представить, что заказ стал недоступен этому юзеру, хотя только что был доступен. Но чтобы понять это, надо залезть в домен и иметь такое понятие как отмененный заказ. и что такие заказы становятся недоступными.
Ошибка, что заказ в принципе не может быть запущен заново, если отменен, вне зависимости от всяких авторизаций выглядит гораздо более логично. Поэтому я и не понял 403 ошибку.
 

WMix

герр M:)ller
Партнер клуба
Ошибка, что заказ в принципе не может быть запущен заново, если отменен, вне зависимости от всяких авторизаций выглядит гораздо более логично.
вопрос интерпретации что такое ресурс, заказ (с точки зрения rest) или сама команда cancel
но это можно и на уровне exception сделать

PHP:
class NotAllowedException extends DomainException{}


$order = $repo->get(123);
$order->calncel(...); // throw new NotAllowedException
$repo->store($order);

catch(NotAllowedException $e){
  http_response_code(403);
}
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
ребята, прочитайте расшифровку аббревиатуры REST, пожалуйста - я пересказывать не хочу, а у вас юношеские фантазии пошли уже ... залезть в домен :)

403 я написал просто для примера - можно 405 Method Not Allowed
нет указания, что 403 применяется ко всему объекту целиком - можно и для набора параметров
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@grigori,
а это разве не про GET/POST/PUT/DELETE?

может назовешь это условие, дабы наши "юношеские фантазии" поприубавить
когда заказ отменен - дальнейшая смена его статуса может быть запрещена, так что 405 здесь может быть уместен

не существует научных теоретических условий возврата 400го кода. коды указываются в API
нет логической связи между тем, что я не приводил код, и условием 400го ответа. я не приводил код - это факт. твоя аппеляция к коду неуместна и бессмысленна. Л - Логика
 
Последнее редактирование:

AnrDaemon

Продвинутый новичок
а это разве не про GET/POST/PUT/DELETE?
HTML в принципе задуман, как расширяемый протокол.
Причём как внешне (добавлением новых возможностей), так и внутренне (расширением значения уже существующих, в разумных пределах).
Так что, например, в контексте REST, код 405 может относиться не только к собственно методу, но и к его применению в контексте определённых параметров. Не вижу объективного противоречия.
Кокретную причину отказа всегда можно описать в… ах, да, конечно HTTP/2…
 

Вурдалак

I'd like to model your domain
Вроде и не про свойства тоже. Вот кому, кроме конечного юзера, может быть интересно разбирать PostPublishingErrors? Вроде никому.
Я не вижу связи с https://en.wikipedia.org/wiki/Information_hiding.
IH — это, грубо говоря, то, что мы обычно называем инкапсуляцией в том смысле, что мы скрываем детали реализации. Но капсула может быть прозрачной, поэтому вот любят разделять эти понятия.

Ну это самый простой вариант того, что внутреннее состояние объекта не позволяет произвести действие. Чуть более сложный вариант - попытаться оплатить отмененный заказ.
А как мы это должны обрабатывать? Если там есть бизнес-процесс (сага), который по событию «Не удалось оплатить отменённый заказ» сделает компенсацию, то тогда я бы сделал ошибку в виде события.

Ну какая разница как это называть. Да, я примерно о том же. Есть UI и он не должен позволять производить неправильные действия. Например ту же оплату отмененного заказа. Но есть же и публичные API. И там надо корректно отрабатывать все эти ошибки.
Тут может быть 2 варианта:
1. Состояние в момент отрисовки UI, тогда можно допускать RC.
2. После выполнения действия можно из command bus (да-да) или service возвращать список ошибок/событий, которые возникли. Среди них там может быть и то самое событие «Не удалось оплатить отменённый заказ».
И рисуй что хочешь.

А как нужно генерировать эту 400 ошибку? Я вот генерирую этим самым BusinessException. Точнее ловлю его в глобальном отловителе эксепшенов и отдаю нужный response с 4хх кодом.
На самом деле, лучше не через exception'ы. Я имею в виду, я в своё время наелся говна с таким кейсом, когда исключение на одном уровне вполне себе BusinessException и можно рассматривать как 400, а потом контроллер чуть-чуть поменялся, часть данных стала заполняться автоматически — и тут уж никак не скажешь, что это 400, клиент вообще не виноват. А варианты тут всякие:
PHP:
$this
    ->commandBus
    ->execute(new PayOrder($orderId, ...))
    ->errors()
    ->map([
        function (CancelledOrderCannotBePaid $error) {
            throw new BadRequestException(....);
        },

        function (OrderWasNotFound $error) {
            throw new NotFoundResource(....);
        }
    ]);

// vs

$orderErrors =
    $this
    ->commandBus
    ->execute(new PayOrder($orderId, ...))
    ->errors()
    ->reduce([
        function (OrderErrors $errors, CancelledOrderCannotBePaid $error) {
            return $errors->withCancelled(true);
        },

        function (OrderErrors $errors, OrderWasNotFound $error) {
            return OrderErrors::notFound();
        }
    ], OrderErrors::none());

return new Response($this->render($orderErrors));
В конце концов, для deduplication, можно массив ошибок/событий пробрасывать дальше, а уже в ResponseConverter (или как там эта херня называется) дообработать те, что остались.
 
Последнее редактирование:

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Отношение к Exception-ам определяется наличием опыта съедания говна с ними, как и с middleware.

Я как-то дорабатывал код с exception-ами, когда изменения каталога планировались раз в месяц, а по факту они ВНЕЗАПНО происходят ежедневно, и ежедневное перекачивание каталога на десятки мегабайт тысячами пользователей одновременно кладет сервера и внешний канал ДЦ. Добавить чуть больше логгирования с метриками в такой код проблематично - после отлова исключения остается только контекст самого исключения.
 

Adelf

Administrator
Команда форума
На самом деле, лучше не через exception'ы. Я имею в виду, я в своё время наелся говна с таким кейсом, когда исключение на одном уровне вполне себе BusinessException и можно рассматривать как 400, а потом контроллер чуть-чуть поменялся, часть данных стала заполняться автоматически — и тут уж никак не скажешь, что это 400, клиент вообще не виноват.
Ну тут у тебя контроллер что-то уж больно умный. Сам какие-то данные достает и подставляет в запрос к service layer. Хотя если это все из HTTP-контекста не выходит(т.е. данные сохранились где-то в куках для примеру), то вроде и нормально...
В любом случае, я не вижу большой разницы в эксепшене с тем же $errors и структуре OrderErrors.
И еще, я могу понять когда простейшая валидация отвалилась, там да, некоторым удобно несколько ошибок, к каждому ошибочному полю, чтобы красиво в UI нарисовать красным, но вроде это уже давно не модно, серверно валидировать такие простейшие вещи да и если валидировать, то можно отдельным ValidationException с нужной структурой.
А все остальные случаи... там всегда лишь одна причина для отказа. Order not found - дальше продолжать смысла нет. Order status не тот - тоже смысла нет продолжать. У тебя бывали кейсы когда действительно несколько причин для отказа? Ведь в коде для этого надо все это предусматривать... формируя потихоньку error-ответ.

P.S. уже в который раз замечаю в твоих примерах функциональный подход с парсингом мета-инфы о параметрах функции. Выглядит красиво. Но перевернуть свое мышление и начать применять у себя... что-то как-то не представляю. Надо попробовать :) видимо, работу с рефлексией у вас там делает какая-то отдельная внутренняя мини-либа...
 

Adelf

Administrator
Команда форума
Клиентская валидация не является заменой серверной валидации.
ну давай уж такие истины не будем обсуждать. я говорю лишь об удобном выводе ошибок для каждого поля.
Можно вообще не проверять, но при попытке создать VO Email::create($request['email']); вылезет эксепшен. и его вполне можно отдать в качестве errorMessage. Беда только в том, что оно на первом же поле спотыкнется и не оповестит об ошибках в других полях. Но если мы подразумеваем нормальное UI, то такой ошибки вообще не должно произойти, так что пофиг.
 

Вурдалак

I'd like to model your domain
Ну тут у тебя контроллер что-то уж больно умный. Сам какие-то данные достает и подставляет в запрос к service layer. Хотя если это все из HTTP-контекста не выходит(т.е. данные сохранились где-то в куках для примеру), то вроде и нормально...
Я не понял.

В любом случае, я не вижу большой разницы в эксепшене с тем же $errors и структуре OrderErrors.
Ты имеешь в виду накопить $errors и выкинуть OrdersErrorsException? Ну, технически может разницы и нет. Но семантически смешивать бизнес и технические ошибки иногда становится неудобно. Здесь есть еще одна не самая очевидная проблема: если у тебя несколько вложенных bounded contexts, а пользователь «отвечает» только за самый верхний, то при вылете FooBusinessException откуда-то из глубин мы можем ошибочно решить, что нужно отдавать 400, хотя клиент может быть вовсе не виноват: то есть, одно и то же исключение может быть на уровне API отображено по-разному в зависимости от контекста.

Более того, исключения — это такая сложная штука, когда в одном контексте исключение у нас checked, а в другом оно уже должно рассматриваться как unchecked, поскольку может быть полностью иррелевантно текущему, то есть по-хорошему его нужно оборачивать в другое исключение, если мы хотим такого же поведения, как в Java.

У тебя бывали кейсы когда действительно несколько причин для отказа?
Ну, как тебе сказать. Иногда просто ошибки и события переплетаются. Например, человек вводит неверно код из SMS в N-й раз: с одной стороны, мы должны сообщить об ошибке (CodeWasWrong) (показать капчу, может быть), с другой — инвалидировать сущность (FooRequestWasInvalidated). Если тупо выкинем exception, то до инвалидации не дойдет и будет проблема с безопасностью.
Или, например, человек лайкает девушку в Tinder: он может увидеть как ошибку «Купите Tinder Plus», но одновременно с этим — «Вы сматчились» (но написать не сможете, пока-таки не оплатите Tinder Plus — этот бред я выдумал на ходу, но как вариант).

P.S. уже в который раз замечаю в твоих примерах функциональный подход с парсингом мета-инфы о параметрах функции. Выглядит красиво. Но перевернуть свое мышление и начать применять у себя... что-то как-то не представляю. Надо попробовать :) видимо, работу с рефлексией у вас там делает какая-то отдельная внутренняя мини-либа...
Здесь еще есть такое преимущество: я могу выкидывать исключение, есть нет соответствующего замыкания с нужным событием/ошибкой: это позволит иметь гарантию того, что при добавлении нового события я добавлю обработку везде. В случае с исключениями можно где-то прое#ать try .. catch и получить проблему, например, ложным 400 Bad Request по FooBusinessException, как описывал в самом начале.

Мои пойнты примерно такие:
1. Делать прямой мэппинг FooBusinessException => http error code — это ошибочный подход.
2. Пускать бизнес-ошибки по отдельному полю errors/events в response команды/сервиса — более безопасный подход, и он позволяет достаточно явно семантически разграничивать бизнес ошибки и технические.
3. Несколько ошибок/событий — более гибкий механизм, чем исключения.
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Вот когда Вурдалак пишет то, что я пытаюсь сказать, получается почему-то понятно. :-/ Курсы русского языка пройти, что ли...
 

Вурдалак

I'd like to model your domain
Ну какая разница как это называть.
Разница в том, что PhpStorm \LogicException и \RuntimeException по умолчанию считает unchecked-исключениями и не требует `@throws` или `try .. catch` :)

По поводу «Post already published» я че-то так и не услышал ответа. Но я так понял речь идёт об идемпотентности. А это важная штука. Не буду тыкать пальцем в один известный банк, но у него был (?) такой прикол в интерфейсе, что если ты делаешь перевод на счёт другому клиенту, у тебя лагает Интернет, то появляется окошко: «Что-то не получилось, кажись, попробуем снова?» и «попробуем снова» приводило к ещё одному переводу. А по-хорошему мы должны получить в ответ ровно то, что и в первый раз и перевод должен быть только один, естественно.
Если обобщить, то у нас есть некая команда и список событий, которые снова не возникнут (нельзя убить убитого человека), но хочется видеть в ответе. Для этого я имею опциональный command id (UUID), по которому я могу на несколько часов записать результаты выполнения команды и отдавать всегда одно и то же, даже не пытаясь выполнить команду реально. Command id обычно приходит с клиента, естественно, потому что только он может сказать новый это запрос или попытка повторить старый лаганувший.

Для всяких OrderWasNotFound можно просто флаг «not_found» какой-нибудь отдавать из command bus, отдельного исключения это тоже что-то не заслуживает.
 
Последнее редактирование:

Вурдалак

I'd like to model your domain
Есть UI и он не должен позволять производить неправильные действия. Например ту же оплату отмененного заказа. Но есть же и публичные API. И там надо корректно отрабатывать все эти ошибки. Так что не совсем LogicException.
class CantPublishPostWithEmptyBody extends BusinessException;
Представь, что в PHP появился скалярный тип string<1,..> («строка ненулевой длины»). Наверное, она будет выкидывать TypeError или типа того при пустой строке. Ты будешь ловить TypeError? Или не будешь использовать такой синтаксис? Мне кажется, что assertion'ы можно рассматривать как расширенные type hinting, если хочешь: не нужно это ловить, до этого просто не нужно доводить.

По-моему, у меня какое-то дежавю, раз в полгода обсуждаем, что assertion'ы и клиентская валидация — это разные вещи, но вот снова опять.
 
Сверху