Ідея “тонкий контролер, товстий сервіс” потрібна не для краси, а щоб система залишалась керованою. Контролер — це HTTP-шар: прийняти запит, перевірити доступ, викликати бізнес-дію, повернути відповідь. Коли в контролері з’являються 200 рядків логіки, ви отримуєте три проблеми: важко тестувати, важко повторно використати, і дуже легко випадково зламати існуючі обмеження. Сервісний шар існує, щоб бізнес-логіка була в одному місці, а контролери залишались простими і передбачуваними.
Що саме має бути в “тонкому” контролері. Перше — отримання параметрів маршруту та запиту (але не їхня складна обробка). Друге — авторизація: policy/gate/middleware. Третє — валідація через FormRequest (контролер отримує вже валідовані дані). Четверте — один виклик сервісу, який робить дію. П’яте — повернення відповіді: redirect або view або json. Все. Якщо контролер починає сам будувати складні query, робити транзакції, логувати activity, дергати storage, рахувати суми, розносити платежі — це вже не контролер, це “кухня” в неправильному місці.
Що має бути в “товстому” сервісі. У сервісі живе бізнес-операція: створити/оновити документ, змінити статус, провести, розпровести, опублікувати, відправити на модерацію, підв’язати медіа, перерахувати агрегати. Сервіс відповідає за порядок кроків, за транзакцію, за цілісність даних між кількома таблицями, за доменні правила (“не можна опублікувати без slug”, “не можна провести без складу”, “не можна змінити суму у posted”). Якщо операція торкається 2+ таблиць — це майже гарантовано має бути сервіс з транзакцією.
Далі ключове: “зрозумілі методи” — це про те, як ви називаєте і структуруєте сервіси. Погані назви — це коли все називається handle() або process() або doSomething(). Хороші — коли метод читається як дія: createDraft(), updateDetails(), submitForReview(), approve(), publish(), cancel(), post(), unpost(), attachMedia(). Тоді контролер виглядає як сценарій, а не як код-каша: “перевірив доступ → викликав publish → відредіректив”.
Дуже важлива дисципліна: один метод сервісу = одна бізнес-операція з чітким результатом. Не робіть метод, який “і створює, і відправляє, і постить, і ще трошки”. Краще дві окремі операції, якщо вони реально різні, або одна операція, але всередині — приватні кроки з зрозумілими назвами. Це дає читабельність і контроль.
Як правильно передавати дані в сервіс. Контролер не має передавати “весь Request”. Він має передати вже валідований масив або DTO. Це важливо, бо сервіс тоді не залежить від HTTP і його легше тестувати. Контролер може взяти validated() з FormRequest і передати. Далі сервіс приводить дані до нормального формату (нормалізація) і працює з ними як з бізнес-входом, а не як з “сирим” запитом.
Де має бути транзакція. Транзакція майже завжди повинна бути в сервісі, не в контролері. Бо транзакція — це частина бізнес-операції (“або все записалось узгоджено, або нічого”). Якщо транзакцію залишити в контролері, ви ризикуєте, що хтось інший викличе сервіс в іншому місці і забуде обгорнути транзакцією. У результаті — роз’їхались таблиці. Тому правило: якщо сервіс пише в 2+ таблиці або змінює критичний стан — транзакція всередині сервісу.
Де має бути лог активності й нотифікації. Activity log — у сервісі, синхронно, як частина факту зміни стану. Нотифікації — як побічний ефект: сервіс може диспатчити job або кидати domain event, який підхопить listener. Важливо: нотифікації не мають бути причиною провалу основної транзакції, якщо це не критично. Тобто “запис зберегли”, а “email не пішов” — це окремий інцидент, не відкат бізнес-операції.
Як це виглядає у типових контролерах. Index-контролер: отримав фільтри → передав у QueryFilter або сервіс “list” → повернув view. Store/update: прийняв FormRequest → викликав сервіс create/update → redirect. Change status: викликав publish/approve/cancel → redirect. Тобто контролер стає сценарієм, а не місцем, де “варять” бізнес-логіку.
Чому це важливо саме для навчання. Новачок, який пише “все в контролері”, швидко робить “щось працює”, але потім він не може підтримувати. Кожна нова фіча — це ризик зламати старі фільтри/доступи/валідацію. Коли логіка у сервісі, ви можете: повторно використати її в CLI/cron/jobs, легко тестувати, і робити акуратні розширення, не ламаючи контракт.
Найчастіші помилки в цьому патерні. Перша — зробити “GodService”, який робить все на світі. Це така сама проблема, як “GodController”, тільки в іншому файлі. Лікується розбиттям сервісів по доменних операціях або сутностях. Друга — залишити частину правил у контролері, частину в сервісі, частину в Blade. Це створює неузгодженість. Третя — передавати в сервіс невалідовані дані і потім ловити баги “інколи падає”. Тому FormRequest і нормалізація — обов’язкові.
Висновок: тонкий контролер — це чистий HTTP-шар, товстий сервіс — це бізнес-операція з транзакцією і доменними правилами, а “зрозумілі методи” — це конкретні назви дій, які читаються як сценарій. Якщо ви тримаєте цей стандарт, код стає передбачуваним: легше додавати фічі, легше тестувати, і головне — значно важче випадково зламати існуючі фільтри, доступи та обмеження.