<? phpukraine СТАТТІ
Пошук по платформі
ТЕСТУВАННЯ 1 вересня 2026 · 4 хв читання

Архітектурні тести в Pest: як заборонити Illuminate у домені й перестати про це сперечатись

Домовленості про шари живуть у README, доки хтось не імпортує фасад у value object. Показуємо, як phpukraine тримає межі модульного моноліту шістьма тестами, що падають на code review, а не в продакшені.

МЛ
Михайло Левицький
PHP-розробник, засновник phpukraine

Кожен проєкт із шарами має одну й ту саму проблему: домовленість «домен не залежить від фреймворку» живе в голові автора і в README. Через три місяці новий колега імпортує Illuminate\Support\Str у value object, бо так зручніше, і ніхто не помічає. Архітектурні тести перетворюють домовленість на червоний CI. У цій статті реальний набір із phpukraine: чотири тести, які тримають межі модульного моноліту на Laravel.

Що саме ми забороняємо

phpukraine побудований як модульний моноліт: src/Hiring, src/Content, src/ContentOps, src/Seo, кожен з шарами Domain, Application, Infrastructure, плюс src/Shared для спільних дрібниць на кшталт відмінювання числівників і годинника. Laravel живе в app/: контролери, Livewire, провайдери, Filament.

Правила прості:

  • Domain не знає про Laravel. Жодного Illuminate, жодного App\. Агрегат Job або value object Salary мають працювати в чистому PHP.
  • Application не знає про Infrastructure. Use case залежить від порту JobRepository, а не від EloquentJobRepository.
  • Веб-шар не лізе в Infrastructure напряму. Контролер отримує інтерфейс через контейнер і не знає, що за ним Eloquent.
  • Shared не знає нікого. Спільне ядро не залежить ні від фреймворку, ні від контекстів.

Це і є текст тестів, майже дослівно.

Тести

Pest має вбудовані архітектурні тести з Pest 2, і вони читаються як речення:

arch('domain layers stay framework-free')
    ->expect(['PhpUkraine\Hiring\Domain', 'PhpUkraine\ContentOps\Domain', 'PhpUkraine\Seo\Domain'])
    ->not->toUse(['Illuminate', 'Livewire', 'App']);

arch('shared kernel stays framework-free')
    ->expect('PhpUkraine\Shared')
    ->not->toUse(['Illuminate', 'Livewire', 'App', 'PhpUkraine\Hiring', 'PhpUkraine\ContentOps']);

arch('application layers do not reach into infrastructure or the web layer')
    ->expect(['PhpUkraine\Hiring\Application', 'PhpUkraine\ContentOps\Application'])
    ->not->toUse(['PhpUkraine\Hiring\Infrastructure', 'PhpUkraine\ContentOps\Infrastructure', 'App\Http', 'App\Livewire', 'Livewire']);

arch('the web layer never touches infrastructure directly')
    ->expect(['App\Http', 'App\Livewire'])
    ->not->toUse(['PhpUkraine\Hiring\Infrastructure', 'PhpUkraine\ContentOps\Infrastructure']);

Плюс два дрібних, які економлять нерви на review:

arch('domain code uses strict types')
    ->expect('PhpUkraine')
    ->toUseStrictTypes();

arch('no debugging leftovers')
    ->expect(['dd', 'dump', 'ray', 'var_dump'])
    ->not->toBeUsed();

Шість тестів виконуються за секунду і запускаються разом із рештою сьюту. Коли хтось додає use Illuminate\Support\Facades\DB у доменний клас, CI каже точно, який файл і яке правило.

Що ці тести змусили змінити

Найцінніше в архітектурних тестах не те, що вони ловлять, а те, як вони змінюють дизайн. Кілька прикладів із проєкту.

Годинник. Домен рахує, чи вакансія прострочена. Спокуса написати now() або Carbon::now() величезна, але обидва варіанти тягнуть Laravel у домен. Тест падає, і зʼявляється інтерфейс Clock у Shared з двома реалізаціями: SystemClock для продакшену і FrozenClock для тестів. Побічний ефект: тести на життєвий цикл вакансії стали детермінованими, без Carbon::setTestNow.

Множина. «5 вакансій», «2 вакансії», «1 вакансія». Перша версія хелпера жила в app/Support і використовувала Str. Домен теж хотів формувати такі рядки для SEO-пояснень. Результат: Shared\Text\Plural на чистому PHP, який використовують і домен, і Blade.

Переписування вакансій моделлю. Use case RewriteFlow в ContentOps\Application спершу викликав Process із Symfony напряму. Це не Laravel, тест не падав, але це інфраструктура в application-шарі. Ми винесли виклик claude -p за порт JobRewriter, і в тестах use case працює з FakeJobRewriter за мілісекунди.

Filament як свідомий виняток. Адмінка працює з Eloquent-моделями напряму, і це порушує правило «веб-шар не торкається інфраструктури». Замість того, щоб загортати кожен ресурс у репозиторії, ми записали виняток у ARCHITECTURE.md і не включили App\Filament у тест. Архітектурний тест має описувати реальні правила, а не ідеальні.

Чого тести не ловлять

Варто чесно сказати про межі.

  • Семантику залежностей. Тест бачить use, але не бачить, що use case зробив три запити до бази через порт, який мав би бути одним. Це ловлять лише unit-тести з фейками.
  • Анемічну модель. Домен може бути вільним від Laravel і водночас бути мішком геттерів. Тест на це не скаржиться.
  • Циклічні залежності між контекстами. Hiring може використати Content, а ContentHiring, і жоден із тестів вище цього не помітить. Для цього потрібне окреме правило на кожну пару контекстів, і його варто додати, щойно зʼявиться перша спокуса.

Як почати у своєму проєкті

  1. Встановіть Pest 2 або новіший; архітектурні тести вбудовані, плагінів не треба.
  2. Напишіть одне правило: expect('App\Domain')->not->toUse('Illuminate'). Воно впаде. Список файлів, які його порушують, і є вашим планом рефакторингу.
  3. Не ставте правило, якого не готові дотримуватись. Краще три чесних тести й один записаний виняток, ніж десять правил, які всі обходять через @skip.
  4. Додавайте правило щоразу, коли на review вдруге виникає одна й та сама суперечка. Тест закінчує суперечку назавжди.

Усі шість тестів наведені вище повністю, їх можна скопіювати у свій проєкт як є. Про те, коли шари взагалі виправдані, є окрема картка: Коли DDD виправданий, а коли шкодить.

ПИШЕТЕ ПРО PHP?Опублікуйте розбір або історію з проєкту на платформіРедактор із чеклістом, редактура, авторська сторінка. Републікація з блогу отримує canonical на оригінал. Відкрити редактор →
МЛ
Михайло Левицький
PHP-розробник, засновник phpukraine
Пише на PHP і Laravel, будує phpukraine як PHP-платформу України. Розповідає про рішення, які реально працюють у продакшені, і про граблі, на які наступив сам.
оновлено 3 вересня 2026 · ліцензія CC-BY-SA-4.0
ДАЛІ ПО ТЕМІ
ЧИТАТИ ДАЛІ
← Усі статті