<? phpukraine PHP-ПЛАТФОРМА
⌕Пошук по платформі
КОНЦЕПЦІЯ DDD · КРОК 7 З 10

HexagonalPorts & Adapters

Ядро застосунку говорить зі світом лише через порти, інтерфейси, які воно оголосило саме. База, пошта, годинник і HTTP підключаються адаптерами, і їх можна замінити, не чіпаючи ядра.

5 хв читання Middle → Senior
01 · ЩО ЦЕ

Ядро, яке не знає, куди пише

Ідею описав Алістер Кокберн: у застосунку є ядро з логікою, і воно не залежить ні від того, хто його викликає, ні від того, куди воно пише. Шестикутник на схемах просто зручна форма, кількість сторін нічого не означає.

Ядро оголошує , тобто інтерфейси, якими воно користується. реалізує порт для конкретної технології: Eloquent для бази, Laravel Mail для пошти. Ядро знає лише порт, а котрий адаптер підставити, вирішує контейнер.

СЛОВНИКПорт. Інтерфейс на межі ядра, названий мовою ядра: ContactNotifier, Clock.
СЛОВНИКАдаптер. Реалізація порту під конкретну технологію: MailContactNotifier шле лист через Laravel Mail.

Адаптери стоять по два боки. викликає ядро: HTTP-контролер, Livewire-компонент, Artisan-команда, тест. викликає саме ядро: репозиторій, пошту, годинник. Тест може бути ведучим адаптером, а фейки в памʼяті веденими, тож ядро перевіряється без бази й мережі.

СЛОВНИКВедучий бік. Driving side: адаптери, що викликають ядро. Контролер, консольна команда, тест.
СЛОВНИКВедений бік. Driven side: адаптери, які викликає ядро. База, пошта, годинник, сторонні API.
02 · МЕЖА

Ядро і два боки

ЯДРОsrc/Contact: Domain і Applicationhandle()LivewireContactFormSubmitContactMessagehandle()«interface»ContactMessageRepository«interface»ContactNotifier«interface»ClockEloquentContactMessageRepositoryMailContactNotifierSystemClockу тестахFrozenClock
ЯДРОsrc/Contact: Domain і ApplicationLivewireContactFormSubmitContactMessagehandle()«interface»ContactMessageRepository«interface»ContactNotifier«interface»ClockEloquentContactMessageRepositoryMailContactNotifierSystemClockу тестахFrozenClock
Контактна форма PHP Ukraine. Ведучий адаптер ContactForm викликає сценарій SubmitContactMessage, сценарій бачить лише три порти, а Eloquent, пошта й годинник підключаються знизу. FrozenClock підставляють у тестах.
Абстракція Ключовий учасник Домен Інфраструктура Клієнт
  • Ядро (src/Contact: Domain і Application) містить: SubmitContactMessage, ContactMessageRepository, ContactNotifier, Clock.
  • ContactForm використовує SubmitContactMessage (handle()).
  • SubmitContactMessage використовує ContactMessageRepository.
  • SubmitContactMessage використовує ContactNotifier.
  • SubmitContactMessage використовує Clock.
  • EloquentContactMessageRepository реалізує ContactMessageRepository.
  • MailContactNotifier реалізує ContactNotifier.
  • SystemClock реалізує Clock.
  • FrozenClock реалізує Clock.
  1. 1Порт оголошує ядро й називає його своїми словами: ContactNotifier, а не MailerInterface.
  2. 2Ядро залежить лише від портів. Адаптер залежить від порту, ніколи навпаки.
  3. 3Привʼязку порту до адаптера робить конфігурація. У Laravel це сервіс-провайдер.
  4. 4Для тестів у порту є свій адаптер, як FrozenClock для Clock.
03 · ЯК У PHP UKRAINE

Сценарій, який бачить лише порти

Сценарій контактної форми отримує три порти в конструкторі й не знає, що за ними Eloquent, Laravel Mail і системний годинник. ContactServiceProvider привʼязує ContactMessageRepository до EloquentContactMessageRepository, а ContactNotifier до MailContactNotifier. Clock спільний для всіх модулів і живе в src/Shared.

php SubmitContactMessage.php фрагмент, скорочено
1final readonly class SubmitContactMessage
2{
3 public function __construct(
4 private ContactMessageRepository $messages,← порт
5 private ContactNotifier $notifier,← порт
6 private Clock $clock,← порт
7 ) {}
8
9 public function handle(array $input): ContactMessage
10 {
11 $message = ContactMessage::submit(
12 reason: ContactReason::from((string) ($input['reason'] ?? '')),
13 name: (string) ($input['name'] ?? ''),
14 email: (string) ($input['email'] ?? ''),
15 company: isset($input['company']) ? (string) $input['company'] : null,
16 pageUrl: isset($input['page_url']) ? (string) $input['page_url'] : null,
17 message: (string) ($input['message'] ?? ''),
18 now: $this->clock->now(),← час з порту
19 );
20
21 $this->messages->save($message);← зберегти
22
23 try {
24 $this->notifier->notify($message);← сповістити
25 } catch (Throwable) {
26 // Stored already; a broken mailer must not turn a sent message into an error page.
27 }
28
29 return $message;
30 }
31}
04 · ТИПОВІ ПОМИЛКИ

Як порти й адаптери зазвичай псують

✕ 1

Порт мовою технології

Інтерфейс з методами where() і paginate(). Ядро знову знає про базу, тільки тепер через інтерфейс.

Натомість: порт описує потребу ядра: save(), findById(), notify().

✕ 2

Інтерфейс на кожен клас

Кожен клас має свій інтерфейс з єдиною реалізацією, навіть доменний сервіс без жодних зовнішніх залежностей.

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

✕ 3

Фасади в ядрі

Сценарій викликає Mail::send() чи now(). Порти формально є, але тест усе одно потребує Laravel.

Натомість: час через Clock, пошта через порт, як ContactNotifier. Фасади живуть лише в адаптерах.

05 · ДЕРЕВО РІШЕНЬ

Чи потрібен вам порт

1
Чи говорить код з чимось зовнішнім, що може змінитися або впасти?
База, пошта, сторонній API, системний час.
НІ →Порт не потрібен: код без зовнішніх залежностей і так тестується.
2
Чи хочете ви перевіряти логіку без цієї зовнішньої системи?
FrozenClock замість справжнього часу, масив замість бази.
НІ →Викликайте фреймворк напряму й тестуйте feature-тестами. Для простого CRUD це чесний компроміс.
3
Чи можна описати потребу ядра двома-трьома методами його мовою?
ContactNotifier::notify(), а не повний API поштового сервісу.
НІ →Межа ще не ясна. Почніть з конкретного класу й виділіть порт, коли зʼявиться другий адаптер чи тест.
ТАК НА ВСІ →Вам потрібен порт. Оголосіть інтерфейс у ядрі, а реалізацію покладіть в інфраструктуру.
06 · СЛОВНИК
Порт
Інтерфейс на межі ядра, названий мовою ядра: ContactNotifier, Clock.
Адаптер
Реалізація порту під конкретну технологію: MailContactNotifier шле лист через Laravel Mail.
Ведучий бік
Driving side: адаптери, що викликають ядро. Контролер, консольна команда, тест.
Ведений бік
Driven side: адаптери, які викликає ядро. База, пошта, годинник, сторонні API.
07 · ПЕРЕВІР СЕБЕ

Питання зі співбесід

Усі питання з архітектури →

У домені або в шарі застосунку, бо порт належить ядру. Тоді інфраструктура залежить від ядра, а не навпаки.

Шари впорядковують код згори вниз, і в класичній схемі все залежить від інфраструктури внизу. Hexagonal ділить на ядро й зовнішній світ: і контролер, і база підключаються адаптерами через порти. На практиці їх поєднують, як у PHP Ukraine: шари всередині модуля, порти на межі ядра.

Тест стає ведучим адаптером і викликає сценарій напряму, а ведені порти отримують фейки: репозиторій на масиві, FrozenClock. Справжні адаптери перевіряють окремими інтеграційними тестами проти бази чи API.