Exceptions как и с чем

Вурдалак

Продвинутый новичок
Представь, что у тебя онлайн-игра. В силу разных причин, ты можешь захотеть ввести в игру ботов (для тестов, по «легитимным» причинам, для имитации активности, ...). Первая идея — напрямую херачить запросы в базу, дергать инфраструктуру, ну, мы же бота пишем, это машина, все дела. Потом приходит понимание, что полезнее использовать те же команды, что используют реальные игроки: во-первых, это будет естественнее и «честнее» (твой бот будет иметь те же возможности, что юзер и не сможет напрямую отнимать жизни у других и т.д.), во-вторых, это проще в поддержке, так как сама игра на ботов не будет завязана и не будет делать различий между ботом и человеком. То есть, логика ботов будет в отдельном bounded context.

И вот теперь представь, что ты вместо слова «команда» используешь понятие «рут» (route), «рутинг». Боты «ходят» по «маршрутам»? Выполняют «маршруты»?

он естественно для консольных приложений и хттп действий разный, но и в том и в другом случае вызов одинаковый
Это наивное предположение, что routing web по количеству «маршрутов» будет один-в-один совпадать с консольным и с набором бизнес-команд, хотя в общем случае это не так. В силу разных причин (BC, компромисс с client-side или просто потому что для внешнего клиента это будет другим действием) у тебя может появится web route, по которому будет внутри выполняться физически 2 бизнес-действия. Здесь web API служит своего рода ACL (anti-corruption layer) между реальными бизнес-командами и внешними.

Вот ещё пример с онлайн-игрой: клиент может захотеть нажать «logout», в то время как в контексте игры мы выполним действие «выйти из игры». В контексте web-сайта это действительно может быть «логаут», в контексте игры — «признать поражение / выйти / ливнуть». А ещё мы можем забанить юзера, но таким образом, чтобы он этого не подозревал (какой-нибудь спамер). Мы ему будет говорить: «окей, чувак, сообщение отправлено» и нагло ему врать в лицо и команда даже не начнёт выполнение.

DRY головного мозга — это когда «похожие» вещи называются «повторами» и «одними и теми же». Это лечится со временем, вот некоторые интересные посты:
http://us3.campaign-archive2.com/?u=1090565ccff48ac602d0a84b4&id=5132867f6e
http://verraes.net/2014/08/dry-is-about-knowledge/
(можешь просто погулять на тему «DRY is evil» и что-то в этом духе).
Безусловно, команды в web-контексте могут по большей части совпадать с бизнес-командами, но нельзя их выравнивать один-ко-одному, они не одинаковые. Web API — просто посредник между внешним миром и внутренней логикой, которая может очень сильно эволюционировать. А web API в силу BC это сделать может не всегда.

чаще всего (практически всегда) консольные команды имеют совершенно другие задачи нежеле web там не надо копипастить.
... или это связано с тем, что тебе тупо неудобно переносить команды из web в консоль, поэтому у тебя «практически никогда» такого не происходит. Я чисто из удобства различные действия а-ля «забанить юзера» дублирую в консольные команды, чтобы можно было это выполнить там, не заходя в админку (которая может требовать подтверждения через SMS для логина и т.д.). Или потому что я не занимаюсь вёрсткой и иногда web-админки тупо нет. :)
 

WMix

герр M:)ller
Партнер клуба
Мне понравился пример с ботом, хочется подумать.
в этом месте
PHP:
$router['RenameUser']->handle(['id'=>42, 'name' => 'Alice']);
вероятнее всего я сам себя обманул
правильно было бы написать
PHP:
$router['RenameUser']->handle(['id'=>42], ['name' => 'Alice']);
в том смысле, что в http эти 2 значения из разных источников. другими словами мыслю RESTом
Код:
POST /user/42/rename
name=Alice
как такое (делить данные индентификации и команды) решать на уровне консольной строки сильно не задумывался.
Если я правильно понимаю, ты сочиняешь на ходу
на самом деле let it be, есть проект который пишу, есть проект который сочиняю, большая часть это живые данные, но есть и запчасти которые хочется сделать лучше. обсуждения очень помогают вылавливать критические моменты, отбрасывать глупости, задумываться о великом :)

идея не думать о транзакции (каждая команда автоматически unitofwork) я до сих пор нахожу правильной, консольная может быть такой же. при необходимости запустить 5 отдельных транзакций, придется выкручиваться через событие.

то что в action и команды приходит готовая сущность мне также кажется разумным. это решает больше чем обернуть исключения в http-коды, это отбрасывает непонятную сущность типа request из общей логики. возможно я просто переложил задачу action на middleware а по сути получил те же яица только в профиль. что имею от этого - запись типа domainAction(DomainEntity $e, ...$data), не думаю о транзакции, отвязываюсь от http-контекста. что теряю - middleware придется на каждый контекст писать заново, в конфигурации можно сэкономить, если добавить правило: они называются одинаково но в разных папках.
а вообще хочется попробовать написать одну и туже команду которая сможет отработать в разных окружениях, возможно пойму больше
 

WMix

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

WMix

герр M:)ller
Партнер клуба
Эээ, command handler? Application, не привязанный ни к одному из внешних форматов.
есть такая штука _format у нас есть 4 helpe'ра
.json, когда просто json_decode вызвал,
.html это в шаблон выкинул,
.pdf это в \tcpdf налепил
.csv для extends \Traversable

/user/42.json это пользователь - привязан тоесть
 

Вурдалак

Продвинутый новичок
> $router['RenameUser']->handle(['id'=>42, 'name' => 'Alice']);

либо запускает все middleware, где проверяются права, специфичные для какой-то конкретной точки (web, админка, CLI, ...) и стартует транзакция,
либо ни один middleware, а это значит не стартует транзакция.
...
Или оно работает как-то по-другому?
По поводу этого:
....
Ты что-нибудь скажешь?
Возвращаюсь к вопросу снова и снова, потому что ты постоянно игнорируешь мой вопрос: что делает $router['RenameUser']->handle()? Я ни разу не пользовался Laravel. Я без малейшего понятия, как этот кусок кода работает. Ты можешь объяснить или так и дальше будешь тыкать в этот кусок кода, считая его произведением искусства?
 

WMix

герр M:)ller
Партнер клуба
PHP:
$router = [
  'RenameUser' => new class {
     public function handle( $identity, $data){
       // если command приблезительно так
       $this->beginTransaction();
       $result = $this->controller->rename( $this->middleware->convert( $identity ), $data );
       $this->commit();
       // иначе
       $this->view->render( 'rename-form', $this->controller->renameForm($this->middleware->convert( $identity )) );
     }  
  },
  // ...
];
Я ни разу не пользовался Laravel.
это не о нем, часть симфонии присутствует но не о них
 

WMix

герр M:)ller
Партнер клуба
в силексе так, грубовато но сложно все описать
PHP:
$app->post('/user/{id}/rename', function (User $user, $name) {
    $this->service->rename($user, $name)
})->convert('id', function ($id) use ($mapper){ return $mapper->byId( $id ); });
 

WMix

герр M:)ller
Партнер клуба
что делает $router['RenameUser']->handle()
словами. router это часть application который маппит подобные руты '/user/{id}/rename' в вызов комманды/action (di для controller). есть возможность добавить один или несколько before/after функций (middleware) как конверторы так и фильтры.
обычно на 1 request отработает 1 рута, но из view может последовать каскадные вызовы других рут (без http вызовов).

PHP:
$router['<ALIAS>']->handle()
просто вызов этой руты из глубины
 

fixxxer

К.О.
Партнер клуба
Я, может, чо не понял, но у меня сложилось впечатление, что чувак просто переместил обработку исключений куда-то в другое место и сделал вид, что если этого другого места прям щас на экране не видно - то его нет.

> Why am I handling domain exceptions in my user interface code?”
Ну потому что @throws exception это такая же часть контракта, как и @return.

> I am not a fan of using exceptions to manage flow control
Угу, и потому поменял шило на мыло. В go есть подобная конструкция (return err, value), но нет исключений. В итоге весь код обрастает проверками на err по всему стеку вызова. А чтобы тот самый стек прокинуть, изобретают всякие страшные штуки.

Еще очень интересно, что это такой за domain service, который почему-то возвращает некий payload. Получается, только потому, что "не нравятся исключения", контракты домена приводятся к какому-то особому виду, где в @return всегда некий payload. Потом все это обрастет какой-нибудь библиотекой и получится какая-нибудь тесносвязанная фигня типа yii.

Если же это на самом деле не domain service, как заявлено, а этакий фасадоадаптер, тогда он просто переложил кусок кода из одного класса в другой и все.

> Once you do that, you’ll realize the majority of your response-building logic can go into a common or base Responder.
Из возможности некоего base responder следует, что количество кодов ошибок конечно для любого domain, что явно ложно (и не надо про custom - в таких случаях всегда будет лень их писать и будет стремление свести все к тому самому "стандартному" набору). А если пытаться все свести к конечному числу, рано или поздно появится что-то вроде парсинга текста error message, когда понадобится различать "похожие" ошибки.

С таким же успехом все можно свести к конечному набору базовых исключений и засунуть try-catch в base responder. Разницы никакой; с исключениями даже гибче, потому что от исключений можно наследоваться.
 
Последнее редактирование:

флоппик

promotor fidei
Команда форума
Партнер клуба
Мне сначала показалось, что пейлоад это HttpResponse, но я щас перечитал, и понял, что это не так, и что он и правда прямо из домена прилетает.
 

Yoskaldyr

"Спамер"
Партнер клуба
Ну если код по ADR (от того же Paul M. Jones), то отдача из доменного слоя исключительно пейлоада хорошо работает. Но многим в зарубежном пхп сообществе не нравится как adr, так и сам автор, учитывая как он любит троллить (хотя и сделал для пхп сообщества дофига и больше, чего стоит та же аура)
 

fixxxer

К.О.
Партнер клуба
Ничего не слышал ни про ADR, ни про Ауру. Щас нагуглил https://github.com/pmjones/adr, смотрю readme.md, ну, аксиома Эскобара какая-то: было: привет-из-2003-года-якобы-mvc, стало: примерно то же самое, только когда дерьмо разложено по коробочкам иначе :)
 

WMix

герр M:)ller
Партнер клуба
у меня сложилось впечатление, что чувак просто переместил обработку исключений куда-то в другое место и сделал вид, что если этого другого места прям щас на экране не видно - то его нет.
Ну и я не думаю, что для http-валидации вообще уместно выбрасывание исключений.
немного противоречиво,


ну он так и написал,
Essentially you move from a try/catch block and exception classes, to a switch/case block and status constants.
какие альтернативы? если бросать исключение во время валидации, то все ошибки не соберешь. с возвратом статуса, это хоть и возвращение к
Код:
define TRUE = 1;
но решает указанную проблему полностью

мне идея не нравится, но пока не понимаю как делать иначе
 

fixxxer

К.О.
Партнер клуба
немного противоречиво,
Никаких противоречий, ты путаешь две разные валидации просто.
На уровне http-приложения - валидируется http-реквест, там будет будет какой-нибудь валидатор. На уровне domain model - тупо Assert::email($email). Ловить в этом случае ничего и не надо - если дошло до assert-а, значит, unhandled exception, 500-я, в логи и вот это все.

Под теми исключениями, которые надо ловить, я имею ввиду не assertion-ы и прочую банальную валидацию, а исключения, специфичные для данного domain и явно объявленные в @throws. Скажем, игра в преферанс, и при добавлении игрока, если их уже четыре, кидается исключение "ПятогоИгрокаПодСтол".
 

Yoskaldyr

"Спамер"
Партнер клуба
@fixxxer, просто аура это был первый фреймворк с независимыми компонентами, и автор был первым кто начал продвигать независимость компонентов у фреймворков, был одним из основателей PHP-FIG, те же PSR1 и 2 полностью его, а PSR4 под его редакцией.
 

WMix

герр M:)ller
Партнер клуба
@fixxxer, те грубо говоря controller выглядит так

PHP:
class UserService{
    /**
     * @throws AccessDeniedException
     * @throws NotFoundException
     */
    function rename($id, $name){
        $this->mapper->getById($id)->rename($name);
    }
}

class UserController{
    /**
     * @throws BadRequestHttpException
     * @throws NotFoundHttpException
     * @throws AccessDeniedHttpException
     */
    public function renameAction(Request $request){
        $form = $this->getRenameForm($request);
        if(!$form->isValid()){
            throw new BadRequestHttpException($form->getMessages());
        }
        // эта портянка может быть уровнем ниже
        try{
          return $this->service->rename($request->get('id'), $form->get('name'));
        }
        catch(NotFoundException $e){
          throw NotFoundHttpException( ..., $e);
        }
        catch(AccessDeniedException $e){
          throw AccessDeniedHttpException( ..., $e);
        }
    }
}
 

AnrDaemon

Продвинутый новичок
бросать исключение во время валидации
Стоп-стоп-стоп.
Ошибка ВАЛИДАЦИИ ВО ВРЕМЯ этой самой валидации - это нифига не исключительная ситуация.
Всё равно что выбрасывать исключение при каждой проверке ACL.
Вот по РЕЗУЛЬТАТАМ валидации вполне возможно выбрасывание исключения, если невалидный ввод - это исключительная ситуация.

А за "try { return …; }" вообще надо памятник при жизни ставить. Причём поверх оригинала, чтобы не вылез.
 

AnrDaemon

Продвинутый новичок
Нет, принципиально - ничего плохого.
Просто хорошего тоже немного.
 
Сверху