Skip to content

paaneko/symfony-blog-ddd

Repository files navigation

Symfony End-Tier Blog

Modular Monolith + Hexagonal + DDD + EDA + CQRS

src/Blog/Article
├── Application
   ├── Dto
   ├── Repository
   ├── Service
   ├── Transformer
   └── UseCase
├── Domain
   ├── Entity
   ├── Event
   ├── Exception
   ├── Repository
   ├── Type
   ├── ValueObject
   └── config
└── Infrastructure
    ├── Controller
    └── Repository

Project Initialization

Для побудови, встановлення залежностей та запуску проекту виконайте:

make init

Bounded Contexts and Modules

Bounded Context: Auth

Module: User

Модуль для взаємодії з користувачем, основна entity всього бізнесу. У різних контекстах користувач може називатися по-різному (e.g., автор, покупець, клієнт).
Однак усі ці назви - просто аліаси до користувача. Якщо іншим модулям потрібно отримати інформацію про користувача або виконати будь-яку дію з ним, вони повинні звертатися до цього контексту.
Наприклад: Article використовує root aggregate id юзера під назвою AuthorId.

Bounded Context: Blog

Module: Article

Модуль для взаємодії зі статтею, основний модуль бізнесу. Він є кластером, і всі його entity повинні вважатися єдиним цілим. Цей кластер має один і тільки один root aggregate - Article, який містить в собі entity Comment. Коментар не може бути окремим модулем або root aggregate, бо він не може існувати без Article.
Щоб уникнути порушення бізнес-invariant та забезпечити атомарність, розробник повинен дістати root aggregate і викликати метод додавання на Article і зберегти в одній транзакції.

Use Cases:
@see Add Comment Handler
@see Duplicate Article Handler

Для отримання інформації об агрегатах з інших BC's в Consumer модулі створюється класс репозиторію який може діставати данні з будь-якого контексту. Так як у нас зараз моноліт, для пришвидшення розробки ми достаємо данні з Shared Database і для того, щоб залишити можливіть лекого переходу на мікросервісну архітектуру, ми всі репозиторії імплеменуємо через загальний інтерфейс який повертає DTO, з яким вже працюють Queries та Handlers.

Module: Category

Модуль категорії не є кластером, оскільки він має тільки один root aggregate - Category. Категорія може існувати самостійно, навіть без жодної статті. Також ми інкапсулюємо всю логіку взаємодії з цим агрегатом.

Module: Section

Так само як і категорія, секція має тільки один root aggregate - Section. Однак, стаття може не належати до жодної секції.

Shared

Цей модуль не належить до бізнесу і не несе ніякої бізнес-цінності. Це набір загальних об'єктів, які може використовувати будь-який модуль у контексті Blog.
Наприклад, модулю Article потрібно дізнатися, чи існує категорія, id якої він отримав через API. Це можливо за допомогою провайдерів.

Use Cases: @see Category Id Provider, Section Id Provider

Також в Shared знаходяться aggregate root id всіх модулів в контексті Blog, взявши звідти id модуль-споживач отримує можливість використати aggregate root id, при цьому не торкнувшись доменного слою модуля.

Bounded Context: Image

Module: Image

Цей контекст може бути названий, як Media у майбутньому. Зачача - займатися менеджментом медіа всього сайту (e.g. баннери, відео, колекції картинок). Та забезпечувати хостинг картинок, генерацію URL, стиснення, перетворення форматів (jpg -> webp), створення різних розширень картинок для різних пристроїв. Може містити в собі модулі: ImageCollection, Video і т.д. Поки ми назвемо цей контекст Image, в якому знаходиться модуль Image.

Фронтенд звертається до модуля Image для того, щоб upload картинку. Задача модуля - зберегти картинку в будь-яку файлову систему, та створити посилання в бд (при цьому в стовпчику is_used поставити false). Якщо картинка не використовується жодним іншим модулем (is_used = false), скрипт щотиждня проходиться та видаляє їх з бд та файлової системи.
Модуль використовує event subscriber для оповіщення про використання картинки.

Use Case:
@see On Article Created Event Subscriber

Після отримання івенту про створення статті від модуля Article контексту Blog, Event Subscriber отримує інформацію яка картинка використалася в статті і закидуює комманду на зміну статусу в Message Bus.

Наразі Message Bus працює в синхронному режимі, але в майбутньому можливо включити в асинхроний режим, де всі об'экти типу Сommand будуть серіалізовані та закинуті в чергу, наприклад RabbitMQ, Kafka. Далі Message Broker буде брати з черги певну кількість комманд та розкидувати між воркерами, поки ці комманди не будуть опрацьовані Handler.

Bounded Context: Search

В майбутньому, якщо сайт буде розвиватись і буде створений інший контекст Merch, Shop або щось інше. І буде бізнес потреба зробити пошук, то в контексті Search повинен створитися модуль Merch, який вже буде відображати пошук саме в цьому контексті не торкаючись інших, вже існуючих.

Module: Blog

Цей модуль відображає пошукову систему Bounded Context - Blog. Тут може використовуватись redis, elasticsearch, або щось інше. І найголовніше те, що контекст Blog навіть не знає який рушій використовує пошук, ба більше, він не навіть знає, що у користувачів є можливість шукати блоги в пошуку.

Цей модуль підписаний на івент Article Created Event. І все що йому потрібне, це article title та author name і найголовніше - це унікальний ідентифікатор самої статті, в результаті пошуку цей модуль поверне ключ за допомогою якого фронт вже сходить до модулю Article і забере повну інформацію, щоб відрендерити сторінку.

SharedKernel

Проксі та збірник коду та контрактів, який можуть використовувати модулі будь-яких контекстів. В майбутньмому при переході на мікросервіси, з нього буде створена composer бібліотека.

AggregateRoot - Клас, який дозволяє своїм підклассам (root aggregate) після наслідування, створювати та пуллити доменні івенти.

DomainEventInterface - Інтерфейс доменного івенту, який можуть створювати тільки root aggregate.

AggregateRootId - Клас, який дозволяє своїм підклассам після наслідування, виконувати роль VO-посилання на будь-який root aggregate. Тим самим дозволяючи агрегату в одному контексті використовувати та сворювати аліаси на агрегат іншого контексту.

Use Cases:
@see User Value Object - абстракнтий класс який наслідує Aggregate Root Id. І тепер модуль User контексту Auth використовується в модулі Article контексту Blog під аліасом AuthorId в Root Aggregate Article

P.S. Всі аліаси, створені з цих VO, позбавлені можливості генерувати uuid.

Навіщо це все?

Переваги:

  • Кожен контекст може мати свою группу розробників, які будуть розмовляти та писати код на одному Ubiquitous Language. Це дозволить розуміти експертів в області та швидше розуміти замовників та робити onboarding. Бо не буде такого, що на сайті в блозі автор називається Author, а в коді - User або Customer.
  • Кожна группа працює над одним контекстом, при цьому не торкаючись інших контекстів над якими працюють інші розробники.
  • Можливість легкого переходу на мікросервіси.

Недоліки:

  • Високий поріг входження.
  • Повільна стартова швидкість розробки.
  • Переускладнені зв'язки між різніми сутностями, та різні блокуючі фактори, наприклад один модуль не може напряму використовувати інший.

Aggregates

Всі операції на агрегаті повинні бути атомарні і гарантувати, що після збереження агрегату всі його частини не будуть inconsistency. Тож агрегати повинні притримуватись ACID:

A - Atomic
Всі зміни кластеру агрегату повинні відбуватися через root aggregate. Наприклад додавання Comment до Article повинно відбуватися через Article тільки через нього. І збереження агрегату повинно відбутися в рамках однієї транзакції, не повинно бути такого частинного збереження - все або нічного.

C - Consistency
Описує, що агрегат може мати в собі посилання на інші root aggregates та сам агрегат може використовуватись в інших root aggregates.
Тому всі зміни в агрегат мають відбуватися в рамках однієї транзакції. Це означає, наприклад при видаленні Article потрібно щоб в модулі пошуку та картинок видалилися записи пов'язані з ним.
Але з цього виходить інша проблема, для того, щоб зберегти весь агрегат в одній транзакції - потрібно завантажити всі його частини в пам'ять, зробити зміну, та зберегти в одній транзакції. І це призведе до дуже великого агрегату, який буде налазити на інші Bounded Contexts, притягне залежності в модуль-ініціатора.
Для цього можна використати Message Bus та Event Bus, використання цих компонентів дозволить достигнути consistency данних не відразу, але в кінці ця consistency буде достигнута. Може пройти 1 мілісекунда, година, але данні між агрегатами будуть consistency через деякий час. Але Event Bus та Message Bus самі по собі не можуть гарантувати 100% consistency, для цього існують патерни 2PC та SAGA, які зі своїми перевагами та недоліками можуть гарантувати це.

I - Isolation
Може відбутися так ситуація, що одночасно прийло 2 запити на зміну аггрегату. 1 запит зберіг данні раніше 2 запиту, а 2 запит перезаписав данні 1 запиту. Це все призводить до inconsistency даних. Щоб це пофіксити можна в таблиці тримати версію даних і перед записом до бд перевіряти чи версія при доставанні даних = версії в бд при зберіганні, цей спосіб зазвичай використовують бд по типу MongoDB. Для MySQL та PostgreSQL існують транзакції різного рівня.

D - Durability
Правило описує, що данні повинні бути довговічними та не пропали при збої системи. Це досягається за допомогою бекапів бд.

API Endpoints

WIP Swagger

Вся API документація реалізована за допомогою swagger-php та доступна тут

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages