
"Тонкий контроллер"
- это то, к чему надо стремиться ))). Подразумевается, что контроллер состоит из методов
, а уже тонкость методов в целом
и говорит о тонкости всего контроллера
.
Речь пойдет о "тонких контроллерах"
в PHP и о том - как лично я это вижу(возможно кто то будет со мной согласен, а кто то будет иметь иную точку зрения).
Вообще про "тонкие контроллеры"
- много где слышал и в разные временные периоды изучения веб программирования - возникало разное понимание этой фразы "тонкий контроллер"
, обозначу, что в моем случае - это было практически синонимом - "хорошая практика"
. А вот как именно это реализовать и что именно реализовывать - постараюсь продемонстрировать ниже.Стек
: php, laravel, controller, немного следующего: SOLID, чистая архитектура, DDD, Hexagonal(Onion, порты и адаптеры), CQRS.Испытуемый
: CRUD(создание, чтение, обновление и удаление сущности) записей(post). В основном для примера буду использовать метод обновления записи(update
). Авторизацию и проверку доступа - специально пропущу(в реальном проекте - пропускать не нужно))) )
Описан Route
вот так мы будем попадать в наш метод контроллера update
во всех дальнейших примерах. PUT http://0.0.0.0/posts/123
<?php //web.php
declare(strict_types=1);
Route::put('/posts/{postId}', [PostController::class, 'update'])->name('posts.update');
Controller
Описание контроллера
- зависит от контекста
используемой архитектуры и/или уровня разработчика
(даже если ни какой умышленной архитектуры не используется - это тоже архитектура, в остальном может быть например DDD
).
В общем для всех вероятных ситуаций:
Контроллер — это класс, который обрабатывает HTTP-запросы
(POST, GET, UPDATE, DELETE) и возвращает ответы
(200, 404, 403, 500).
Дальше по статье будем немного видоизменять понимание его и от этого(понимания) уже говорить про "тонкость контроллеров"
- в контексте каждого уровня разработки(по уровням разбил интуитивно, как сам вижу это).
Итоговая реализация
кода в каждом из контекстов
- зависит именно от мотивации
, которой придерживается разработчик
, а мотивация уже в свою очередь - зависит от понимания конечной цели
.
Например: Для того, чтоб сделать "тоньше"
- нужно уменьшить количество строк
в этом методе, а для этого и только для этого
мы выносим правила валидации
в отдельный файл. (Это один из примеров, не является по моему мнению верным понимание)
В каждом из контекстов ниже
опишу свою точку
видения по поводу этого с точки зрения разработчика - занимающегося этой задачей.
Контекст 1: Мой первый проект ))
Дешево
Низкий порог вхождения (знание основ Laravel, php и хватит, "работает - значит все правильно")
Тонкость контроллера
- чем меньше строчек
в методе - тем он "тоньше"
.
Пример до рефакторинга
: В целом работает, но надо "улучшить"
и как то сделать короче
(поменьше строчек)
public function update1(Request $request, int $postId): Response // Без архитектуры
{
// Валидация вручную вместо Form Request
$validator = Validator::make($request->all(), [
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
$data = $validator->validated();
try {
// Поиск и обновление поста
$post = Post::findOrFail($postId);
$post->title = $data['title'];
$post->content = $data['content'];
$post->save();
// Кеширование
\Cache::forget("post_{$postId}");
return response()->noContent(200);
} catch (ModelNotFoundException $e) { // Не нашли пост
return response()->noContent(404);
} catch (\Exception $e) { // Не получилось обновить
return response()->json([
'error' => 'Server error'
], 500);
}
}
Рефакторинг: Для того, чтоб сделать "тоньше"
- нужно уменьшить количество строк
в этом методе, а для этого и только для этого
мы:
выносим правила валидации
в отдельный файл. (случайно сделали код чище)укоротили процесс обновления
поста, ура - наш код стал еще короче (случайно сделали код грязнее)
class UpdatePostRequest extends FormRequest
{
public function rules(): array
{
return [
'title' => ['required', 'min:3', 'max:255'],
'content' => ['nullable', 'min:3'],
];
}
}
public function update1(UpdatePostRequest $request, int $postId): Response // Без архитектуры
{
try {
Post::findOrFail($postId)->update($request->validated());
// Кеширование
\Cache::forget("post_{$postId}");
return response()->noContent(200);
} catch (ModelNotFoundException $e) { // Не нашли пост
return response()->noContent(404);
} catch (\Exception $e) { // Не получилось обновить
return response()->json([
'error' => 'Server error'
], 500);
}
}
Итог контекста:
обновление
записи - теперь напрямую зависит
от названия полей
в HTTP запросе
. А важно ли нам это в текущем контексте
?? Я не уверен. Так что тут с задачей мы справились))).
Контекст 2: UseCase + контроллер = минимализм
Сколько я наделал, сколько переделал, сколько сайтов запилил(Опытный Laravel разработчик, знание некоторых паттернов)
Тонкость контроллера
: не просто короткий
, а минимализм - несколько строк
это моя цель.
Продолжаем обновлять предыдущий код, но теперь мы уже выносим логику прочь из контроллера
Рефакторинг:
обработка исключений - вынесли
формирование ответа для фронта - вынесли
кеширование - вынесли
public function update2(UpdatePostRequest $request, int $postId, PostService $handler): Response // UseCase + контроллер = минимализм
{
return $handler->createOrUpdate($postId,$request);
}
Метод createOrUpdate
описывать не буду, так как в текущей теме - его реализация только отведет в сторону от главной темы про "тонкость контроллеров".
Итог контекста: У нас метод контроллера в одну строку
))) Вот он тончайший и "идеальнейший контроллер" - относительно текущего контекста.
Контекст 3: Разбиение по слоям (конечный вариант)
Тут уже понимание слоистой архитектуры
(слои: Презентация, Приложение, Инфраструктура, Доменная область), разграничение кода по слоям и направление зависимостей в них.
Тонкость контроллера - определяется наличием кода только
из слоя презентации
и только для конкретного клиента нашей системы
. А разбиение кода презентации уже идет относительно других классов этого слоя(презентации).
Например UpdatePostRequest
- в отдельном классе отвечающим за правила валидации
, при необходимости можно отдельный класс для ответа
написать, но именно касаемо слоя презентации.Метод контроллера
- не знает за то, как что то сохраняется(каким образом обновляется запись в БД), кешируется, и в целом взаимодействует в нашей система. Должен уметь только через DTO
используя UseCase
передать на следующий слой
(слой приложения) входные данные
, а дальше конвертирует
ответ в соответствующий контракт
для клиента откуда пришел запрос, например: отдает статус сервера
и какой то json
если это RestApi
(какое то React приложение) или просто вывод текста, если это консольная команда.
То есть, теперь количество строк
- имеет второстепенное значение.
Что мы сделали:
Отдали
UseCase
обновления постовминимально
необходимый набор данных. Добавивпрозрачность
к ожидаемымвходным данным
для слоя приложения(Application layer)Убрали привязку к объектам(уменьшили связность) теперь при необходимости - мы можем обновлять запись из
любого места
, даже там,где нет объекта Request
Обработали исключения на стороне контроллера, то есть какой
ответ
конкретно в этомпредставлении
необходим(для шаблонизатора и для API(реакт например)), так какответы отличаются от клиента к клиенту
(RestApi, Console, Web(Blade и другие шаблонизаторы))В случае, если нужно добавить
особый ответ для клиента в слое представлении
- мы так же составляем его вметоде контроллера
из результата метода$handler->update(...)
. Таким образом нашапрезентация зависит от логики
, алогика уже становится не зависима от презентации
.Улучшаем тестирование - в таком случае - у нас будет единая точка для тестирования - куда мы уже подставляем явные скаляры.
Понимание
ответственности
метода контроллераотносительно слоя
презентации(Получить запрос и вернуть ответ в соответствии сконтрактом
дляконкретного типа клиента
). Соответствуем принципу единой ответственности(SOLID буква S(SRP))
public function update3(
UpdatePostRequest $request,
int $postId,
PostService $handler
): Response // Разбиение по слоям
{
// Получаем входные данные
$requestData = $request->validated();
try {
$handler->update( // Используем DTO для передачи данных на другой слой
new Command( // передаем скалярные данные, только, которые необходимы сервису для выполнения операции
$postId,
$requestData['title'],
$requestData['content'],
)
);
return response()->noContent(200);
} catch (ModelNotFoundException $e) { // Не нашли пост
return response()->noContent(404);
} catch (\Exception $e) { // Не получилось обновить
return response()->json([
'error' => 'Server error'
], 500);
}
}
Вывод:
К конечному пониманию "тонкого контроллера"
пришел эволюционным подходом. Возможно кто то может иметь отличную от моей точку зрения.Контекст 3
- считаю наиболее валидным
использованием этого подхода. Плюсы описал выше.
Так же этот подход является частью реализации Hexagonal
(архитектура построенная по принципу портов и адаптеров
). Где:
Порт
- это UseCase(какое то действие, которое выполняем, использовал CQRS(Там поясняется название классов для DTO) ) - обновить пост
$handler->update( // Используем DTO для передачи данных на другой слой
new Command( // передаем скалярные данные, только, которые необходимы сервису для выполнения операции
$postId,
$requestData['title'],
$requestData['content'],
)
);
Адаптер
- это реализация порта, примеры ниже:
Сам метод контроллера
public function update3(
UpdatePostRequest $request,
int $postId,
PostService $handler
): Response // Разбиение по слоям
Так же это может быть например какой ни будь другой адаптер
например через консоль
хотим обновить пост
/**
* Обновить пост из консоли
*/
public function updateCondoleAdapter(int $postId, string $title, string $content): void
{
$this->postService->update( // Используем DTO для передачи данных на другой слой
new Command(
$postId,
$title,
$content,
)
);
}
P.S. Мнение о том, что "Тонкий контроллер"
- это малое количество строк
- думаю связано с тем, что зачастую в проектах
- можно встретить действительно внушительно многострочные методы контроллеров
, где в одном месте находится вся реализация
необходимой задачи.
И для более легкого порога вхождения(Или что б соответствовать корпоративному стилю, где все методы так описаны)
в оптимизацию
этого метода контроллера как раз и может быть представлено(разработчик может предположить), что уменьшение количества строк = оптимизация
. Конечно косвенно
- это может помочь
, но на практике
- убирает одни сложности и добавляет другие
))
Комментарии (27)
FanatPHP
09.06.2025 09:52Вот вы в прошлый раз отмахнулись, "я не об этом". А здесь снова наступаете на те же грабли. Видимо, дело не в "я не об этом", а в "да я понятия не имею, что это за исключения такие, на курсах сказали try catch писать" (вариант: "ChatGPT написал, а я в душе не чую, что это и зачем").
Вы взялись рассуждать о тонких контроллерах, и при этом у вас на две строчки полезной работы - полтора экрана обработки ошибок, совершенно одинаковой во всех экшенах контролера. Вы сами-то этого слона совсем не видите? Контроллер у вас по-прежнему тонкий? :)
Я вам порекомендую провести один эксперимент. Пробуйте совсем удалить все эти эти блоки try и catch и посмотреть, изменилось ли хоть как-то поведение сайта.
Да, и рассуждая про "разбиение по слоям": почему у вас контроллер, обращаясь к PostService, ловит при этом исключение из совсем другого слоя? Почему он вдруг должен что-то знать про какие-то модели, если обращается к сервису?
Ivan6463 Автор
09.06.2025 09:52Мое видение - это то каждый слой обрабатывает исключения по своему, что касается контроллера то для шаблонизатора это может быть один ответ, а для реакта совсем другой. То есть если не получилось создать пост, контроллер не должен знать об инфраструктуре и о том, с какой именно бд что то пошло не так.
Получается сервис ловит исключения, обрабатывает их и выкидывает новый тип исключений уже для слоя презентации, в котором находится информация именно для презентации без каких либо технических тонкостей.
Насчет обращений - презентация обращается к приложению, потом приложение взаимодействует с доменным слоем используя DI взаимодействует с инфраструктурой
Что по вашему является тонким контроллером? Было бы интересно посмотреть на Ваше понимание с примером и пояснением.
Если а Ларе убрать исключения из контроллера - то под капотом конечно это обработается, но будет уже не явно. По возможности стараюсь уходить от такой магии.
FanatPHP
09.06.2025 09:52Я подозреваю, что весь этот "чистый и прозрачный код" и "отсутствие магии" вы пишете просто для галочки. Вас на самом деле не интересует, как поведёт себя приложение при ошибке. Это распространённая проблема старательных новичков. Они знают, что ошибки надо обрабатывать обязательно-обязательно! Ну и пишут эту обработку в силу своих теоретических представлений.
При этом привыкнув писать только учебные программы с ровно одним экшеном на контроллер, они не задумываются, насколько чудовищно будет выглядеть код более-менее востребованного контроллера, с этими рядами однообразной обработки ошибок, среди которых надо специально выискивать значащий код. Эти ваши повторения - это не "единая ответственность". Это профнепригоднорсть.
Для blade один и образом например, а для React другим?
Я вам не зря посоветовал попробовать и самому посмотреть. Вы вполне могли бы ещё выплыть, просто последовав моему совету. Но вы решили положиться на чат жипити. Штош.
Ivan6463 Автор
09.06.2025 09:52Если Вы знаете как сделать лучше, буду только рад посмотреть примеры кода с обоснованием.
Почему исключения не надо обрабатывать было бы очень интересно послушать. Поясню эти исключения кидаются со слоя приложения, это ни те, которые инфраструктура кидает, а именно со стороны сервиса.
vitiok78
09.06.2025 09:52Всё написанное ниже - просто информация к размышлению.
Можно долго рассуждать о разных принципах проектирования, читать заумные книги о том, как вам супер-чисто усложнить свой код, введя 150 разных слоёв в свой проект, разбить монолит на микросервисы, подключить Кафку для обмена сообщениями, и иметь при этом 10 зарегистрированных пользователей...
Если в вашем проекте подразумевается только один способ общения с внешним миром (например, REST API), то можно хоть всю бизнес-логику запихнуть в контроллер. "Толщина" метода в таком случае крайне редко отрицательно влияет на его читаемость и возможность рефакторинга. Простой последовательный код всегда проще анализировать, чем пытаться это делать прыгая по разным файлам и обратно. И даже улучшением тестирования не стоит оправдывать сильное усложнение кода, т.к. 100%-ное покрытие юнит-тестами сильно переоценено, да и при грамотном внедрении зависимостей можно протестировать и длинный метод.
Однако, как только вы понимаете, что ваш проект будет получать/отдавать одни и те же данные из разных источников (REST, SOAP, RabbitMQ, телепатический канал с Нибиру и т.д.), то тут для понимания минимальной "толщины" контроллера, нам просто нужно понять, что же такое контроллер.
А контроллер - это просто дверь. Дверь, которая пропускает либо не пропускает груз в ваш магазин. А дальше, принимаете ли вы товар с рампы, через главный вход для покупателей, либо через крышу, вступает в действие один и тот же алгоритм: подкатить тележку, погрузить на неё товар, отвезти на склад. Т.е. контроллер и должен выполнять функции двери. А двери бывают разные: с домофоном, с замком, вообще настежь открытыми.
Т.е. в контроллере должны обрабатываться ситуации, связанные исключительно с тем каналом общения, к которому он привязан. Т.е если REST, то проверяем заголовки, аутентификацию (если она не где-то в middleware), верность того же json. Расставляем на всё это безобразие try -> catch, и отвечаем правильно клиенту.
Всё же, что не связано непосредственно с каналом связи, немедленно выносим в отдельных хэндлер, и информацию для этого хендлера формируем одинаково из всех типов контроллеров, которые существуют в проекте. Т.е. формируем "контракт", по которому контроллеры общаются с этим хэндлером. В качестве контракта может быть и DTO-шка, и асинхронное сообщение в очередь, и даже (прости господи) тупой ассоциативный массив. Таким образом, ни контроллеры не знают, как работает там внутри себя хэндлер, и хэндлеру абсолютно пофигу, какими там путями к нему попали данные. К нему данные пришли по контракту, он их по контракту и выполнил.
И дальше в хэндлере у вас может снова быть такой же "толстый", но очень понятный и читаемый метод, который будет с данными делать то, что требуется вашему бизнесу.
К чему я это всё? А к тому, что все эти чистые коды с чистыми гексагональными архитектурами, DDD и прочими теориями - это не закон, а просто подсказка, которая не позволяет нам стать героями мема "А что, так можно было !?!?!?". Все эти методы - просто информационная помощь нам в нашем интересном и непростом труде, который, тем не менее, является всего-лишь инструментом для облегчения работы бизнеса, и ничем более. Мы должны отталкиваться от реальной жизни, а не от каких-то догм.
SolidSnack
09.06.2025 09:52немного следующего: SOLID, чистая архитектура, DDD, Hexagonal(Onion, порты и адаптеры), CQRS.
Где это у вас вообще в статье?) Ткните пальцем в CQRS, ощущение что вы пишите сами не понимая что и зачем, говорите адаптер это реализация порта, но у вас не одного интерфейса не реализуется в примерах ваших... Для кого эта статья?
Ivan6463 Автор
09.06.2025 09:52Реализация интерфейса в контексте «портов и адаптеров» - под интерфейсом может пониматься ни конкретная реализация синтаксиса php, а в целом подход, то есть интерфейс = порт, реализация интерфейса = адаптер.
Да, если мы будем говорит о репозитории- то там все понятнее будет. Интерфейс репозитория(порт) и реализация его(адаптер)
в конце статьи как раз подробнее описывал
SolidSnack
09.06.2025 09:52А я думал под интерфейсом понимается интерфейс, ну я понял, у вас тут все свое, вы это поняли, а кому принесли не понятно
Ivan6463 Автор
09.06.2025 09:52Ну это ни совсем свои, тут сам таким же вопросом долго задавался, когда порты и адаптеры изучал, все ни мог понять, как сделать слой презентации, с репозиториями понятно было, а с презентацией вообще не понимал, потом вот в разных сообществах поднимал этот вопрос и потихоньку пришел к этому решению.
Ну статья изначально холиварная, а мне хотелось узнать мнения с разных сторон насчет моего решения и либо еще больше укрепиться в своем понимании, либо найти более подходящее решение.
DmitriiMikhailov
09.06.2025 09:52Может обработку ошибок лучше в render вынести? И оставить только
public function update1(UpdatePostRequest $request, int $postId): Response { $post = Post::findOrFail($postId); $post->update($request->validated()); \Cache::forget("post_{$postId}"); return response()->noContent(); }
Вот документация
https://laravel.com/docs/12.x/errors
Kwisatz
Вы делаете очень много однотипных приседаний, которые в контроллере не нужны, например ошибки. Если у вас повторяется один и тот же код снова и снова ну вынесите его в базовый цикл жизни вашего приложения.
А тонкий контроллер, ну вот, например
Ivan6463 Автор
Специально решил оставить тут обработку ошибок, что бы показать свои идею о том, что тонкость это не количество строк, а единая ответственность, тут это ответственность- взаимодействие с клиентом через RestApi. К тому же в текущей реализации - обработка становится явной и прозрачной. А насчет дублирования кода - тут тоже нужно смотреть контекст, попытка обьеденить дубликат кода в разных контекстах может породить еще больше проблем и сложностей, а поддержка и обновление его во все усложнят разработку.
Где в Вашем примере будет обрабатываться исключения? Например не найдена сущность и второй вопрос как будете реализовывать если нужно будет для разных клиентов по разному обработать ответ? Для blade один и образом например, а для React другим?
Kwisatz
Сударь, а вы прочитать мой пост или тот что ниже не хотите? Генерация ошибок - удел сервиса/репозитория/etc затем они перехватываются в основном потоке и преобразуются в вид, который понимает фронт. Если для разных клиентов надо обрабатывать ответ вы пишете специфический Response и позволяете клиенту управлять типов ответа, все.
Ivan6463 Автор
Я правильно понимаю, что у вас ответ для фронта генерируется в сервисе(код ответа и сам ответ)? А что касаемо блейд шаблона например? Тоже в сервисе будете возвращать?
Kwisatz
Неправильно
Ivan6463 Автор
Можете тогда продемонстрировать как Вы это делаете?
Kwisatz
Вы на моем скрине можете увидеть как это происходит, контролер получает данные от сервиса и возвращает их диспатчеру, там ответ пакуется в Response (специфичный, если нужно) и отправляется, все.
Ivan6463 Автор
В вашем примере метод контроллера возвращает результат сервиса, то есть на стороне именно сервиса получается формируется ответ для клиента, как по мне это обязанность контроллера
Вот тут кстати есть подобный пример использования сервиса с которым я солидарен. Где сервис возвращает только набор данных, а вот уже контроллер преобразует эти данные в ответ для соответствующего клиента
https://habr.com/p/350778/
Kwisatz
Для какой цели вы задаете вопрос, если не читаете ответ?
vitiok78
Это слишком "тонко". Так тонко, что может "порваться". Получается, что ваш "service" должен знать, в каком виде предоставить ответ клиенту. Он же должен знать, какие исключения надо выбрасывать клиенту, чтобы он их правильно понял. Подумайте, что будет, если вам дадут задание добавлять в базу данные, принимаемые не только по HTTP, но и через gRPC, WebSockets, TCP/UDP от каких-то аппаратов и т.д.? Им ведь понадобится возвращать ответ в каком-то другом виде.
Вы просто разделили контроллер на две части, вынесли вторую часть в service. Это ничего более, чем бессмысленное переусложнение кода.
Ваш service и контроллеры должны обмениваться информацией через контракт.
Kwisatz
Вы все еще не слушаете. Сервису плевать, он не в курсе, он возвращает данные, его ответ оборачивается в Response и отдается клиенту.
По исключениям так же, приложения порождает ексепшены там где они есть.
Если "нам дадут задание", то вместо "JsonRPC 2.0" в запросе, указывается иной протокол, пишется под него request/response и забывается. Про все это уже давно продумано.
А дальше у вас пачка каких то домыслов. Сервис умеет возвращаеть данные, у него есть методы, контролер дергает эти методы, все. Более того, если мы говорим про продвинутые протоколы, наподобие GraphQL - то контролер нафиг не нужен никому. Это по сути своей рудимент эпохи форм и он останется жить там где у нас тупо формочки на сайте.
С чего должны, кому должны? почему должны? Я не очень приветствую апеляций к опыту, но вы уверены, что при контексте "мой первый проект" уместно так рьяно и безапеляционно спорить с людьми у которых опыта 20 лет?
michael_v89
У вас каждый метод сервиса должен проверить параметры на 404 и 403, а скорее всего еще и 400, и выбросить соответствующее исключение. То есть вы повторяете в сервисе ответственность контроллера, только на более абстрактном уровне.
Kwisatz
метод сервиса должен проверить параметры... не понимаю о чем вы. Посмотрите скрин, там видно что какие из параметров required какие у них типы итд Далее инициализируется сущность, валидируется на целостность и лезет в базу. Контроль доступа тоже есть на скрине, это работа ACL, переданные переменные после валидатора и санитайзера дополнительно валидируется на типы данных в сигнатурах методов. Код всегда 200, потому что я терпеть не могу смешивать коды транспортного и уровня приложения. Отсутствие сущности ошибкой не является, у остальных ошибок свои коды, корневой обработчик их перехватывает, раскладывает по группам, преобразует для фронта и сует в сентри. То есть по сути кроме довольно сложно йописанной модели - все остальное просто и тупо и легко можно заменить.
michael_v89
В deleteAction передается id. Что если этого id нет в базе? Контроллер должен вернуть ответ "not found".
В addAction передается partnerId. Что если этого partnerId нет в базе? Контроллер должен вернуть ответ "validation failed".
Ну и такой подход не позволяет вернуть 2 ошибки валидации одновременно для partnerId и hasTransportZone, что бизнес иногда требует, потому что пользователям не нравится исправлять по одной ошибке за раз.
А, да, пропустил, но я имел в виду также ситуации, когда сущность находится в состоянии, в котором ее нельзя редактировать. Статья на модерации например, пока модерация не завершена, пользователь не может редактировать текст. Надо загрузить сущность по id, проверить ее статус и вернуть соответствующий ответ.
Это неважно, я имел в виду их семантические значения, просто написал цифрами для краткости.
Ну я про это и говорю, сервис возвращает разные ответы, то есть выполняет роль контроллера, только на более абстрактном уровне.
Kwisatz
Контроллер не должен и не может валидировать сущность на целостность. Ваша логика описывает обычный подход работы с формами, и в этой парадигме я бы мог с вами согласиться. В моем же мире я давно обмениваюсь с фронтом сущностями и я могу представить что она валидируется в DTO или в модели с валидаторами например доктрины (а у питонистов полно инструментов получше), но представить что все это будет вынесено в контроллер... , ну представить то могу, я такое видел, и вы можете исповедовать, если вам хочется, но я от такого избавился.
Хотя я вот задумался что ошибки валидации модели я таки могу вернуть все разом, просто это требуется примерно никогда потому что у фронта своя валидация ровно такая же и она как раз отрабатывает вся сразу.
И об этом контролер знать не может и не должен. Это уж точно строго дело сервиса.
Про роль контроллера несогласен, но остальное +- верно. Как раз только что беседовал с коллегой, что благодаря подходу по факту логики у меня просто нет, у меня набор правил валидации на уровне модели, сервиса (+ естественно на типы данных но это опустим), базы и... все, больше нет ничего.
michael_v89
Я и не сказал, что должен. Он должен вызвать компонент, который это делает, и преобразовать его результат в ответ, соответствующий протоколу. Возвращать нужный ответ это ответственность контроллера.
Это зависит не от подхода, а от бизнес-требований. Возможно ваши бизнес-требования можно свести к правилам "если валидация входных данных прошла, сохраняем их как есть", но это не всегда возможно.
Kwisatz
Почему то стало казаться что таки всегда. Я сам исследую этот вопрос.