Сервіс у Laravel — це місце, де живе бізнес-логіка, а не “склейка” HTTP. Контролер повинен бути тонким: прийняв запит, прогнав валідацію, викликав сервіс, повернув відповідь. Як тільки ти починаєш писати логіку створення/оновлення прямо в контролері, він роздувається, логіка дублюється, з’являються різні сценарії “майже однаково, але трохи інакше”, і через місяць будь-яка правка стає ризиком.
Для навчального CRUD (Tags/Sections/Resources) сервіс може виглядати як “невелика надбудова”, але саме він формує правильну звичку. У реальному проєкті create/update майже завжди включає додаткові дії: запис активності, синхронізація зв’язків, генерація slug, робота з медіа, оновлення лічильників, логування. Якщо це все залишити в контролері — ти отримаєш комбайн, який ніхто не буде підтримувати.
Правильний розподіл відповідальності виглядає так. Контролер відповідає за HTTP: який маршрут, який метод, який FormRequest, який редирект, яке повідомлення. FormRequest відповідає за валідацію правил (required, max, unique тощо) і, інколи, за базову авторизацію запиту. Сервіс відповідає за сценарій: “створити сутність”, “оновити сутність”, “записати пов’язані речі” і гарантувати, що дані не стануть частково збереженими. Модель відповідає за зв’язки, casts і дрібну нормалізацію атрибутів.
Мінімальний сервіс для CRUD має два методи: create() і update(). Вони приймають вже валідовані дані, а не “весь request”. Це важлива дисципліна: сервіс не має сам вирішувати, які поля дозволені, це робить FormRequest і/або whitelist у сервісі. Якщо ти передаси в сервіс сирий $request->all(), ти повертаєшся до масових ризиків і непередбачуваних полів.
Валідація “входу” в сервісі — це не заміна FormRequest, а друга лінія захисту. Вона потрібна для двох речей. Перше — нормалізація: привести дані до потрібного вигляду (trim, lower, привести дату до формату, привести boolean). Друге — бізнес-правила, які не є “формальними” правилами валідації. Наприклад, “не можна ставити published_at, якщо статус не published”, або “не можна змінювати resource_id після публікації”, або “slug генерується автоматично, якщо не переданий”. Такі речі часто не лягають в прості правила required|max, але критично впливають на цілісність.
Найважливіша тема — транзакції. Транзакція потрібна завжди, коли одна операція змінює 2+ таблиці або кілька пов’язаних станів. Класичний приклад навіть для навчального модуля: ти створюєш тег і одразу записуєш активність “хто створив” у таблицю activity. Або ти оновлюєш розділ і синхронізуєш зв’язки в pivot-таблиці (belongsToMany). Якщо на другому кроці буде помилка, без транзакції перший крок уже запишеться, і ти отримаєш “напівзбережений” стан. Це завжди погано, бо потім важко відловити, чому дані виглядають дивно.
Логіка транзакції проста: або зберігається все, або не зберігається нічого. У Laravel це робиться через DB::transaction, і сервіс — ідеальне місце для цього, бо транзакція описує саме бізнес-операцію. Контролер не повинен думати про транзакції, він повинен думати про відповідь користувачу.
У create-сценарії стандартний потік такий. Сервіс бере валідовані дані, нормалізує їх, створює основний запис, потім виконує додаткові дії: синхронізує зв’язки, записує активність, додає метадані, і лише потім повертає створену модель. Якщо будь-який крок падає — транзакція відкатує все. Це гарантія, що база не перетвориться на “частково записане”.
У update-сценарії стандартний потік трохи інший. Ти завжди починаєш з того, що маєш модель (або її id) і дані. Сервіс визначає, які поля можна оновлювати в цьому сценарії, нормалізує їх, застосовує оновлення, і знову ж таки — якщо є додаткові таблиці або зв’язки, робить це в тій же транзакції. Дуже важлива дисципліна: update не повинен випадково перезаписати поля, які не були в формі. Тому або використовуються тільки ті ключі, що прийшли валідованими, або робиться явний whitelist.
Ще один реальний момент: сервіс — це правильне місце для “центральних” правил, які не можна дублювати. Наприклад, генерація slug. Якщо ти робиш slug у контролері, а потім ще десь — ти неминуче отримаєш різні правила і баги. Якщо slug робиться в сервісі або mutator’і моделі, він завжди буде однаковим. Але важливо не перетворювати mutator на “бізнес-движок”. Нормалізація — в mutator, сценарій — в сервіс.
Як виглядає правильний контролер поруч із сервісом. Контролер приймає FormRequest, бере $validated = $request->validated(), викликає $service->create($validated) або $service->update($model, $validated), і повертає redirect з flash-повідомленням. Це все. Якщо в контролері лишається складна логіка — значить сервіс недороблений або відповідальність змішана.
Типові помилки, які треба одразу прибрати. Перша — робити сервіс, але все одно тягнути $request всередину сервісу. Сервіс не має знати про HTTP. Друга — робити транзакцію в контролері, бо “так простіше”. Це ламає структуру. Третя — робити один “універсальний” метод saveEverything() на 500 рядків. Краще два чіткі методи create/update, і окремі приватні методи всередині сервісу для кроків.
Висновок: винесення create/update в Service — це не “архітектурна мода”, а спосіб тримати проєкт керованим. Контролер стає тонким і передбачуваним, логіка зосереджується в одному місці, її легше тестувати і розширювати. А транзакція в сервісі гарантує, що при роботі з 2+ таблицями ти не отримаєш напівзбережений стан, який потім зламає довіру до даних і з’їсть час на розслідування.