0% прочитано

Коли Repository справді потрібен у Laravel DDD

Репозиторій не потрібен для кожної Eloquent-моделі. У DDD він має сенс як межа збереження aggregate або коли прикладний рівень не повинен знати деталі збереження.

17 жовтня 2026 р. 11 хв читанняЛаравель

Коли я вперше серйозніше дивилася на Repository pattern у Laravel, схема здавалася простою: є Eloquent model, отже поруч має бути repository.

З часом я від цього відійшла. Якщо repository лише повторює find(), save() і delete(), він часто додає шар коду, але не додає нової межі.

У DDD для мене Repository став корисним у конкретнішій ролі: як межа стійкості для агрегату.

Це напряму пов’язано з матеріалі про aggregate і межу узгодженості: якщо aggregate є одиницею, яка захищає інваріант, repository може завантажити й зберегти цю одиницю як ціле.

Repository має сенс не тому, що існує таблиця. Він має сенс, коли application layer потребує доменної операції збереження, а не знання про Eloquent, joins або конкретну схему.

Repository для кожної моделі швидко стає дублюванням

Repository лише повторює Eloquent API
final class ProductRepository{    public function find(int $id): ?Product    {        return Product::query()->find($id);    }     public function save(Product $product): void    {        $product->save();    }     public function delete(Product $product): void    {        $product->delete();    }

Такий клас може бути корисним у конкретному коді, але сам факт його існування ще не дає DDD-переваги.

Він не приховує складний persistence, не представляє aggregate і не дає application layer нової мови. Він просто переадресовує виклики.

Репозиторій стає корисним на межі збереження агрегату.

Контракт описує операції над aggregate, а не над таблицею
interface ProductRepository{    public function get(        ProductId $id,    ): Product;     public function getForUpdate(        ProductId $id,    ): Product;     public function save(        Product $product,    ): void;}

Тут для мене вже важлива не таблиця products, а Product як агрегат.

Його збереження може охоплювати кілька таблиць, пов’язані записи або перетворення даних між об’єктами та структурою бази даних.

Application Service не повинен знати про ці деталі реалізації.

Application Service координує, Repository зберігає

У матеріалі про Application Service і Domain Service я писала, що Application Service координує сценарій, а не володіє всіма правилами.

Те саме стосується збереження даних. Якщо сервіс знає, які зв’язки потрібно завантажити заздалегідь, які таблиці об’єднати через JOIN і які записи оновити окремо, це означає, що межа відповідальності між шарами вже порушена.

Application Service бачить aggregate і намір сценарію
final class ActivateProduct{    public function __construct(        private ProductRepository $products,    ) {    }     public function handle(        ProductId $productId,    ): void {        DB::transaction(function () use (            $productId,        ) {            $product =                $this->products->getForUpdate(                    $productId,

Repository не повинен бути універсальним Query Builder

Якщо Repository копіює Builder, межа стає нечіткою
$repository    ->whereStatus('active')    ->whereCountry('UA')    ->withRegistrations()    ->orderByName()    ->paginate(50);

Для списків, фільтрів, сортування й API-read я часто взагалі не використовую Repository.

У матеріалі про Read Model у DDD я писала, що читання не зобов’язане відтворювати domain model. Тут Query Builder або окремий query object часто простіший і чесніший.

Repository може приховувати складний mapping

Application layer бачить aggregate, а не схему збереження
Application Service        |        vProductRepository        |        +-- products        +-- product_settings        +-- product_rules        +-- mapping        |        vProduct aggregate

Це особливо корисно, коли модель збереження даних і доменна модель не збігаються повністю.

Репозиторій може відновити агрегат із кількох записів бази даних і правильно зберегти його зміни, не змушуючи прикладний код знати деталі структури таблиць і зв’язків між ними.

Repository interface теж не є обов’язковим ритуалом

Я також перестала автоматично створювати interface + implementation для кожного repository.

Інтерфейс корисний, якщо він реально відділяє application/domain code від інфраструктурної реалізації. Якщо контракт нічого не відділяє, додатковий interface може бути лише церемонією.

Repository pattern теж може бути лише архітектурною декорацією

У матеріалі про те, чому DDD не починається зі структури папок я писала, що правильні назви папок самі по собі не створюють DDD.

З Repository те саме: класи з правильними назвами не допомагають, якщо вони лише дублюють можливості ORM і не визначають чітку межу відповідальності за збереження агрегату.

Коли Repository мені не потрібен

Я зазвичай не вводжу окремий Repository, якщо:

  • це простий CRUD без суттєвих доменних правил;

  • Eloquent model уже достатня для сценарію;

  • Repository лише повторить ORM API;

  • операція є чистим читанням для списку або API;

  • немає реальної межі відповідальності за збереження даних, деталі якої потрібно приховувати.

Коли Repository для мене виправданий

Сильні сигнали для Repository для мене такі:

  • Application Service працює з aggregate, а не з таблицею;

  • завантаження агрегату потребує отримання даних із кількох записів бази даних;

  • деталі збереження даних не повинні проникати в прикладний рівень;

  • потрібен окремий метод для отримання агрегату з блокуванням на час оновлення, наприклад getForUpdate();

  • доменна модель і модель Eloquent, що відповідає за роботу з базою даних, почали відрізнятися;

  • агрегат потрібно зберігати як єдину узгоджену одиницю.

У AVN Center я перестала сприймати Repository як обов’язковий шаблон

У роботі над AVN Center я почала дивитися на Repository не як на "правильну DDD-папку", а як на конкретну межу.

Якщо сценарію треба отримати aggregate, виконати доменну поведінку і зберегти результат, не знаючи деталей реалізації механізму збереження даних, Repository працює дуже добре.

Якщо задача просто повернути таблицю з фільтрами, я не змушую її проходити через той самий abstraction. Внутрішні repository та aggregate AVN Center тут навмисно не показую.

Моя коротка перевірка перед створенням Repository

Перед створенням Repository я питаю:

  • Що я зберігаю: aggregate чи просто row?

  • Чи application layer знає забагато про Eloquent або SQL?

  • Чи Repository приховає реальну складність, а не лише додасть зайвий рівень перенаправлення викликів?

  • Чи це write-side задача, а не звичайне читання?

  • Чи контракт виражає доменний намір?

  • Чи код стане зрозумілішим після появи цієї межі?

Repository потрібен не для кожної моделі, а для правильної межі

Для мене Repository у Laravel DDD перестав бути обов’язковим шаром між controller і Eloquent.

Його цінність з’являється там, де application layer працює з aggregate і не повинен знати, як цей aggregate зібраний або збережений.

Для простих CRUD і read-side запитів прямий Eloquent або Query Builder часто чесніший і простіший.

Я додаю репозиторій не просто тому, що існує модель. Я додаю його тоді, коли є чітка межа відповідальності за збереження агрегату, яку варто зробити явною.

Repository для кожної Eloquent-моделі додає шаблон. Repository для aggregate додає межу. Для мене це головна різниця.