Межі між шарами зазвичай описані в документі: домен не знає про Laravel, контролер не ходить у базу, модуль Billing не читає таблиці модуля Hiring. А потім хтось поспішає й додає в доменний клас Config::get(). Unit-тести зелені, бо поведінка правильна, ревʼю дивиться на логіку, і межа зникає без жодного сигналу. Архітектурний тест перетворює таке правило на перевірку, що падає в CI з назвою класу-порушника.
У Pest це arch() з expect() простору імен і очікуванням: not->toUse(), toOnlyUse(), toOnlyBeUsedIn(), toUseStrictTypes(), а для класів toBeFinal(), toExtend(), toImplement(), toHaveSuffix(). Pest розбирає файли PHP-парсером і нічого не виконує, тож тест не потребує бази й іде за секунди. Залежністю Pest вважає і use класу, і виклик функції, тому expect(['dd', 'dump'])->not->toBeUsed() ловить забуті дебаг-виклики. Для типових правил є пресети php(), security(), laravel(), strict(), relaxed().
Найцінніші правила охороняють те, що дорого відновлювати: домен без фреймворку, Application без Infrastructure, модулі, що спілкуються лише через публічний API, контролери без прямих запитів. Перевіряти смак (суфікс Service у кожного класу, заборона слова Manager) можна, але такі правила частіше дратують, ніж захищають.
У архітектурних тестів є дві відомі діри. Перша: глобальні хелпери Laravel now(), config(), app() не мають простору імен, тож not->toUse('Illuminate') їх не бачить, і доменний клас з now() проходить правило. Їх треба перелічити поруч або взяти білий список toOnlyUse, який дозволяє лише назване. Друга: одруківка в просторі імен (App\Domian) нікого не знаходить, і правило вічно зелене. Тому, написавши правило, його один раз ламають навмисно й дивляться на червоний тест. Окремо стежать, щоб новий модуль не опинився поза правилом: зірочка в App\Modules\*\Domain покриває всі модулі одразу.
Поза Pest ту саму задачу розвʼязують Deptrac, де шари й дозволені напрямки описують у конфігу, і PHPArkitect з правилами на PHP. У легасі-проєкті будь-який з інструментів вмикають одразу, а наявні порушення вносять у явний список винятків (ignoring() у Pest, skip-конфіг у Deptrac): нові класи перевіряються з першого дня, а борг видно і його можна скорочувати.
// tests/Architecture/LayersTest.php
// Домен будь-якого модуля: зірочка бере один сегмент простору імен,
// тож новий модуль потрапляє під правило сам.
arch('domain stays framework-free')
->expect('App\Modules\*\Domain')
->not->toUse([
'Illuminate',
'now', 'config', 'app', 'request', // хелпери без простору імен
]);
arch('application does not reach into infrastructure')
->expect('App\Modules\*\Application')
->not->toUse('App\Modules\*\Infrastructure');
arch('controllers do not query the database directly')
->expect('App\Http\Controllers')
->not->toUse('Illuminate\Support\Facades\DB');
arch('no debugging leftovers')
->expect(['dd', 'dump', 'ray', 'var_dump'])
->not->toBeUsed();
// Готові правила: eval, exec, md5, rand, unserialize...
// Свідомий виняток описаний явно, а не вимкнений пресет.
arch()->preset()->security()->ignoring('md5');
Розкажіть, яке правило ви б поставили першим: «домен не імпортує фреймворк і хелпери» з `toOnlyUse` або з повним списком заборон. І додайте практику: щойно написавши правило, я один раз ламаю його навмисно, щоб переконатися, що тест справді червоніє.