Повний модуль — це коли у вас є не “екран”, а завершена функція системи, яку можна віддати в прод і не боятися, що вона відкриє дані або зламається від першої правки. У Laravel такий модуль завжди складається з однакових блоків: UI (список + форма), фільтри, доступи, сервісний шар, activity log, тести. Якщо ви засвоїте цю конструкцію один раз, далі ви будете повторювати її для будь-якої сутності: теги, розділи, ресурси, користувачі, документи, платежі, медіа.
Починаємо з правильної “рамки” модуля. Спочатку визначте сутність і її мінімальний життєвий цикл: які поля обов’язкові, які статуси, хто може створювати/редагувати/архівувати, що вважається “видимістю”. Це не бюрократія — це те, що потім ляже у policy, валідацію і фільтри. Новачкова помилка — спочатку верстати форму, а потім думати “а хто це може бачити”. Правильний порядок — спочатку правила, потім код.
Далі будуємо UI каркас: дві сторінки і два сценарії. Перша — index: таблиця зі списком, кнопка “створити”, рядкові дії “редагувати/архівувати”, пагінація. Друга — форма create/edit: поля, кнопки “зберегти/скасувати”, показ errors і flash. Важливо: UI має бути максимально тупим — він лише відображає те, що йому передали. Не пхаємо в Blade доступи або бізнес-логіку, тільки @can для кнопок і читання даних.
Фільтри — це наступний шар. Для index-сторінки ви робите фільтри як GET-параметри (query string), щоб стан сторінки жив у URL. Мінімальний набір майже завжди: q (пошук), status, date_from/date_to, інколи user_id або resource_id. Правило: спочатку базове обмеження видимості (доступи), потім фільтри, потім сортування, потім paginate. Так ви гарантуєте, що фільтр не розширює доступ, а лише звужує. У UI фільтри мають підставляти поточні значення з request() і не губитися в пагінації.
Політики доступу — критичний блок. Вам потрібно закрити два типи доступів: доступ до дії (create/update/delete/change status) і доступ до видимості (які записи користувач взагалі може бачити). Дія закривається policy методами типу viewAny, view, create, update, delete, restore, forceDelete, плюс спеціальні дії (наприклад publish, post, archive). Видимість закривається або через policy + scoped query (наприклад visibleTo($user)), або через окремий query scope в моделі/репозиторії. Для вашої архітектури (де важливі обмеження й фільтри) — обов’язково мати “базовий scope видимості” і ніколи його не обходити. Це захист від витоків.
Після цього виносимо бізнес-логіку в сервіс. Контролер має бути тонким: отримав FormRequest → authorize → викликав сервіс → повернув відповідь. Сервіс робить основну операцію: create/update/archive/change status. Якщо операція торкається 2+ таблиці (а з activity log майже завжди так), сервіс повинен використовувати транзакцію. У сервісі ж робиться нормалізація входу (trim, приведення типів, дефолти) і доменні перевірки, які не зручно тримати у валідації (наприклад, “не можна архівувати, якщо є активні залежності”).
Activity log — це частина “дорослого” модуля. Його пишуть тоді, коли змінюється стан сутності або відбувається важлива дія: створено, оновлено, змінено статус, архівовано/видалено, прикріплено медіа, змінено відповідального. Запис має містити: хто (actor), що (action), над чим (entity_type/entity_id), коли (timestamp), і бажано request/correlation id. Важливо: activity log пишеться синхронно в сервісі в межах транзакції (або одразу після успішного commit), щоб не було “лог є, а зміни не збереглись”.
Тепер — тести. Модуль без тестів у навчанні — це пастка: він наче працює, але будь-яка правка через тиждень його ламає. Мінімум для “повного модуля” — 7–10 feature-тестів, але реально стартувати можна з 6–8, якщо вони покривають критичні гілки. Базовий набір: доступ без прав (403), доступ з правом (200), видимість “не бачить чужі”, фільтр по статусу працює і не розширює доступ, create з валідними даними створює запис і пише activity, create з невалідними даними не створює запис, update працює і пише activity, заборонений update (наприклад archived/posted) не проходить. Якщо є архівація — тест на архівування і тест, що archived не показується у звичайному списку (або показується тільки з фільтром).
Щоб це не було “теорією”, уявіть модуль “Sections” або “Resources” як навчальний приклад. UI: index зі списком + create/edit форма. Фільтри: q по назві, status active/archived. Policies: лише редактор/адмін може змінювати, автор — тільки дивитись. Service: create, update, archive. Activity: section.created, section.updated, section.archived. Тести: 1) гість/без прав — 403, 2) редактор — 200, 3) фільтр status працює, 4) create валідний — ок, 5) create невалідний — помилки, 6) update пише activity, 7) автор не може update, 8) archived не показується без фільтра. Це і є “повний модуль” у мініатюрі.
Окремо про дисципліну “не ламати існуюче”. Коли ви робите модуль у живому проєкті, ви не переписуєте існуючі фільтри/обмеження “під себе”. Ви інтегруєтеся: використовуєте вже прийняті конвенції маршрутів, вже існуючі middleware, вже прийняту структуру views, існуючі helpers. Якщо в системі є правило “account_id/company_id scoping” — воно має бути в кожному запиті. Будь-яке відхилення — це майбутній баг або витік даних.
Висновок: повний модуль у Laravel — це повторювана конструкція, а не “творчість”. Спочатку правила і видимість, потім UI каркас, потім фільтри (GET + query string), потім policies (дії + видимість), потім сервіс з транзакцією, потім activity log як аудит, і в кінці — feature-тести на доступи/фільтри/валідацію/статуси. Якщо ви зможете зробити один такий модуль самостійно, ви фактично переходите з рівня “верстаю екрани” на рівень “будую систему, яка живе і не розвалюється”.