Что за мода пошла? REST-like HTML CRUD

Adelf

Administrator
Команда форума
Уже не первый раз замечаю, что народ, особенно в Laravel, начал делать обычные HTML CRUD приложения, используя контроллеры для REST. Эмулируя PUT и DELETE методы(ларвель это умеет) получается почти REST, но через HTML.
Я этому очень плохо отношусь, но раз народ делает... хочу знать зачем. Там банально неудобно.. каждой форме свой экшен вешать(у меня почти все формы идут на ту же страницу, но через POST. привык уже).
Мож тут тоже кто-то такой же? Расскажет зачем. Все эти эмуляции через эмуляции для меня... как надувные женщины :)
 

флоппик

promotor fidei
Команда форума
Партнер клуба
Уже не первый раз замечаю, что народ, особенно в Laravel, начал делать обычные HTML CRUD приложения, используя контроллеры для REST. Эмулируя PUT и DELETE методы(ларвель это умеет) получается почти REST, но через HTML.
code-generation + sane method naming + automatic api implemetation.
Ну и программисты же, не для людей делают, для машин и для других программистов.
 

c0dex

web.dev 2002-...
Команда форума
Партнер клуба
Зафигачил у себя CRUD, 4 метода для сущности. Отдельные шаблоны. Удобно. Но метод PUT я не эмулирую, только DELETE через AJAX запрос.
 

WMix

герр M:)ller
Партнер клуба
@Adelf, это html эмулирует PUT и DELETE a Laravel их великолепно понимает как и эмуляцию. а идея как раз написать и на голом html и с поддержкой js
 

WMix

герр M:)ller
Партнер клуба
это как отрендишь у меня контроллеры отдают array a дальше стратегия _format и можно в json завернуть
 

Adelf

Administrator
Команда форума
А ну это уже интересней. Просто я вижу это в контроллерах, которые отдают сразу HTML view... и не понимаю зачем там этот REST-like стиль.
 

WMix

герр M:)ller
Партнер клуба
делаешь config
id => [route, controller, view]
отлавливаешь событие у меня onKernelView на silex
a дальше if( $event->getRequest() ... )
 

Вурдалак

Продвинутый новичок
В качестве оффтопика: вас никогда не посещала мысль, что король-то голый REST иногда выглядит сомнительно? Вы работаете с API, который выглядит, как анемичная модель. Вы ограничены HTTP-глаголами GET, PUT, POST, DELETE, то время как вам нужно ими отражать реальные бизнес-действия.

Как пример: вы хотите забанить юзера. Что вы будете делать? Писать POST /users/42 {"banned": true}? Или POST /users/42/ban?
Если допустить, что первое, то такой API становится достаточно скудным: вы не знаете какие действия возможно таким образом описать, потому что нельзя просто так выставлять какие-то свойства, в реальности-то будет вызываться какой-нибудь $user->ban(), а не $user->setBanned(true). Я думаю, не нужно объяснять, почему это так.
Если второе, то это приведет к тому, что вы просто будете писать POST /users/42/<commandName>, т.е. вы будете выполнять те же команды, только записанные, вероятно, в менее удобном формате. Почему бы тогда просто не написать POST /commands/ {"command": "BanUser", "payload": {"id": 42}} и не выводить список всех таких команд в документацию?

Я для себя пришёл к тому, что для запросов на изменение нужно работать с чем-то а-ля JSON RPC.
 
Последнее редактирование:

Adelf

Administrator
Команда форума
Мне всегда RPC вариант нравился больше. Мы всегда просим API сделать что-нибудь. Выбрать сущности, добавить, удалить(не говоря уже о том, что написал Вурдалак). Т.е. главное - глагол. И процедуры под это подходят больше. Чем тщательно спрятанные в HTTP-методы глаголы.
 

WMix

герр M:)ller
Партнер клуба
Писать POST /users/42 {"banned": true}?
или POST /users/42 {"name": 'Pupkin'}
до тех пор пока возможно почему бы и нет... но и параллельно POST /users/42/ban ничего не мешает сделать я не понимаю твою проблему
Вы ограничены HTTP-глаголами GET, PUT, POST, DELETE
яб наоборот сказал, для этих 4х уже придумывать ничего не надо

али тебя CRUD в рамках держит?
 

Вурдалак

Продвинутый новичок
или POST /users/42 {"name": 'Pupkin'}
OK, а теперь выяснится, что юзер не может быть переименован, если он заблокирован:
PHP:
final class User
{
    public function rename(Name $newName) {
        Assertion::false($this->banned, 'Banned user cannot be renamed');
        $this->name = $newName;
    }
}
Что твой метод должен вернуть при запросе POST /users/42 {"name": "Blah", "banned": false}? С точки зрения клиента может показаться, что ошибка о блокировке неуместна, т.к. одновременно с переименованием я посылаю флаг разблокировки, но каким образом внутри мы можем понять в каком порядке нужно выполнять соответствующие бизнес-действия, т.е. что сначала нужно выполнить UnbanUser, а лишь затем — RenameUser?

Далее, теперь представь, что мы отправляем юзера на перемодерацию, если он сменил имя:
PHP:
final class User
{
    public function rename(Name $newName) {
        Assertion::false($this->banned, 'Banned user cannot be renamed');
        $this->name = $newName;
        $this->status = Status::moderation();
    }
}
Сам по себе метод /users/42 будет возвращать скорее всего и name, и status. При этом при переименовании ты меняешь одно свойство, но в результате меняются 2. Более того, нужно каким-то образом дать понять клиенту, что поля неравнозначные и он не может напрямую менять status, т.е. это read-only поле.

Т.е. возникает leaky abstraction: якобы ты можешь работать с ресурсом, как с пачкой свойств, но в реальности всё сложнее.

Далее возникнут проблемы документирования: если для одних действий могут быть одни ошибки, для других — другие, то метод POST /users/42 будет содержать кучу возможных errorCode'ов, в то время как на, допустим, то же переименование может быть только 2 errorCode'а, но клиенту придётся поддерживать все 10, т.к. неочевидно какие из них когда могут «выстрелить». Один большой жирный анемичный метод, который делает хрен сколько всего. Не круто.

но и параллельно POST /users/42/ban ничего не мешает сделать
Это неконсистентность. Такой API труднее понимать. То так, то эдак.
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Ещё один пример в догонку: а теперь нужно сделать метод на принудительную отправку юзера не перемодерацию. Внимание вопрос: каким образом этот метод будет выглядеть? POST /users/42/ {"status": "moderation"}? Выглядит так, словно мы можем менять статус напрямую? Это не так. Это просто такой синтаксис для команды SendUserForModeration. Это неочевидно.

Также иногда возникают различные способы создания сущности. К примеру, обычная рега и через партнёра. В случае команд будет две явные команды: RegiserUser, RegisterUserViaPartner. Будет различие в содержимом команд, вторая будет требовать partnerId. А в REST это будет одинаково: POST /users/. Это тоже неочевидно.
 

WMix

герр M:)ller
Партнер клуба
POST /users/42 {"banned": true}?
это голый crud
$user->update($post,$id); те. тут предполагается что banned один из аттрибутов
какой ответ придумал, тот и возвращай всегда

POST /users/42/ban
а это твой
$user->ban()

дальше хоть whiteliste на POST /users/{id} в аттрибутах чтоб только 2й метод разрешить.

в смысле я не вижу проблемы ограгничения
 

Вурдалак

Продвинутый новичок
$user->update($post,$id); те. тут предполагается что banned один из аттрибутов
какой ответ придумал, тот и возвращай всегда

POST /users/42/ban
а это твой
$user->ban()
Вот именно, что у меня не будет update(), у меня будет список конкретных действий: rename(), ban(), etc. У меня будут только методы POST /users/42/<commandName>, не будет никакого POST /users/42/. О чём я и говорю, нафиг тогда делать вид, что это REST, когда JSON RPC тут ложится куда лучше?
 

fixxxer

К.О.
Партнер клуба
В качестве оффтопика: вас никогда не посещала мысль, что король-то голый REST иногда выглядит сомнительно?
Посещала. Поэтому везде, где бэкенд сложнее, чем этакий http-доступ к базе данных с аутентификацией -авторизацией и row-level security, по возможности использую json-rpc. Аргументировал ровно так же.
 

WMix

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

Вурдалак

Продвинутый новичок
ну не пошагово же менять данные
Почему нет?

Можно, если так припрёт, сделать команду, которая обновляет различные данные юзера. Можно, например, сделать команду Update/ChangeProfile, где обновлять имя, фамилию и так далее. Но когда тебе нужно подписываться на конкретные события: юзер был забанен, юзер был разбанен (и ему требуется компенсация за время бана), то всё равно ты приходишь к тому, что внутри эти команды будут fine-grained: будет отдельно метод ban(), будет отдельно метод unban(), changeProfile() и т.д.
 
Последнее редактирование:

WMix

герр M:)ller
Партнер клуба
ну те твой контроллер похож на
user->rename($post[name]);
user->moved($post[address]);
user->reborn($post[birthdate]);

а как на счет атомарности? ну дело не в пользователе, это же и заказы, деньги?
 
Сверху