Правило, яке перевіряє збірка
У ARCHITECTURE.md написано: домен не знає про Laravel. Потім хтось поспішає й додає в доменний клас Config::get(). Unit-тести зелені, бо поведінка правильна, рев'ю дивиться на логіку, і межа зникає непомітно. перевіряє саму будову коду: які класи використовує простір імен, чи класи final, як вони названі.
У Pest такий тест починається з arch(). Далі expect() з простором імен і : not->toUse(), toOnlyUse(), toOnlyBeUsedIn(), toUseStrictTypes(), а після classes() ще toBeFinal(), toExtend(), toImplement(), toHaveSuffix(). Pest розбирає файли PHP-парсером і нічого не виконує, тож база й контейнер не потрібні. Залежністю він вважає use, повне імʼя класу просто в коді й виклик функції.
Для типових правил є : php забороняє var_dump(), print_r(), die і схожі, security забороняє eval(), exec(), md5(), rand(), unserialize(). Свідомий виняток описують через , а не вимикають правило. Альтернативи поза Pest: і , обидва запускаються окремою командою в CI.
Pest читає код і звіряє залежності з правилом
- App\Hiring\Domain містить: Job, SalaryRange.
- LayersTest використовує Pest arch (expect()).
- Дані йдуть від Pest arch до App\Hiring\Domain.
- Job використовує App\Shared\Clock.
- SalaryRange використовує Illuminate\…\Config.
- Pest arch створює Звіт.
Одна межа, пʼять способів її записати
Модуль вакансій: домен у App\Domain, порт JobRepository, Eloquent-реалізація в App\Infrastructure\Persistence, привʼязка в сервіс-провайдері. Перемикайте вкладки: межа та сама, змінюється те, з якого боку на неї дивиться правило.
Запустіть і подивіться
-
1
рядки 3–18
Тест працює з текстом файлів, а не з обʼєктами. Тому йому не потрібні ні Laravel, ні база, ні навіть існування класу Config.
-
2
рядки 19–31
SalaryRange не має use, але згадує \Illuminate\...\Config повним іменем просто в коді, і без declare(strict_types=1). Пошук лише по рядках use цього б не помітив.
-
3
рядки 49–53
Правило: простір імен і заборонені префікси. Так само влаштований not->toUse([...]) у Pest.
-
4
рядки 55–76
Збираємо залежності: рядки use і повні імена класів. Pest робить те саме PHP-парсером, тому знаходить ще й виклики функцій.
-
5
рядки 78–81
Префікс порівнюємо до межі сегмента: App\Hiring\Domain не має збігатися з App\Hiring\DomainEvents.
-
6
рядки 83–109
Кожне порушення називає файл, рядок і заборонене імʼя. Червоний тест у CI зупиняє злиття й деплой.
- Наведіть на пояснення, щоб підсвітити рядки.Торкніться пояснення, щоб підсвітити рядки.
LayersTest тримає модульний моноліт
Модулі PHP Ukraine лежать у src/, кожен зі своїми Domain, Application і Infrastructure. Що з чим може говорити, вирішує не рев'ю, а цей файл: він проганяється разом з усіма тестами перед кожним деплоєм. Домен і спільне ядро не бачать Laravel, Livewire й app/, сценарії не бачать інфраструктури, вебшар не бере інфраструктуру Hiring, ContentOps і Projects напряму, у всьому PhpUkraine є strict_types, а dd() і dump() не доживають до продакшну.
Як архітектурні тести брешуть
Одруківка в просторі імен
not->toUse('Iluminate') чи expect('App\Domian') проходять зеленими: імені, якого не знає автозавантажувач, Pest просто не знаходить, і порушувати нічого. Тест є, а правила немає.
Натомість: написавши правило, один раз зламайте його навмисно й подивіться на червоний тест. Для шарів, які майже ні від чого не залежать, беріть toOnlyUse: одруківка в його списку робить тест червоним.
Хелпери проходять повз правило про Illuminate
now(), config(), app() і request() це глобальні функції без простору імен. Доменний клас з now() проходить not->toUse('Illuminate'), хоча звʼязок з Laravel той самий, що й через фасад.
Натомість: перелічіть хелпери поруч із простором імен: ->not->toUse(['Illuminate', 'now', 'config', 'app']). Pest вважає виклик функції такою самою залежністю, як use класу.
Новий модуль поза тестом
Правило перелічує простори імен явно, як LayersTest на цьому сайті. Хтось додає модуль Billing, а його Domain не потрапляє в жоден expect(), і межа в ньому не перевіряється.
Натомість: зірочка в імені покриває один сегмент: expect('PhpUkraine\*\Domain') бере домен кожного модуля, і новий модуль потрапляє під правило сам.
Фільтр, що діє лише на перше очікування
expect('App\Shared')->classes()->toBeFinal()->toHaveSuffix('Service'). classes() відсіяв інтерфейси для toBeFinal(), а toHaveSuffix() у тому самому ланцюжку знову бачить усе, і падає на інтерфейсі Clock, якого правило не стосувалося.
Натомість: одне очікування після classes(), implementing() чи extending() на один arch(). Два правила з однаковим expect() читаються не гірше й перевіряють саме те, що написано.
Чим перевіряти межі у вашому проєкті
- Архітектурний тест
- Тест на будову коду, а не на поведінку: залежності між просторами імен, модифікатори класів, назви. Падає з назвою класу, який порушив правило.
- Очікування
- Перевірка в ланцюжку expect(). Для архітектури в Pest є toUse, toOnlyUse, toOnlyBeUsedIn, toBeFinal, toExtend, toImplement, toHaveSuffix, toUseStrictTypes і десятки інших.
- Пресет
- Готовий набір правил: arch()->preset()->php(), security(), laravel(), strict(), relaxed(). Застосовується до просторів імен з autoload у composer.json.
- ignoring()
- Виключає з правила клас, простір імен або залежність. Працює і для окремого arch(), і для пресету: ->preset()->security()->ignoring('md5').
- Deptrac
- Окремий інструмент: шари описують у конфігу, а Deptrac перевіряє, що залежності між ними йдуть лише дозволеними напрямками.
- PHPArkitect
- Окремий інструмент з правилами на PHP у власному конфігу. Зручний, коли тести на PHPUnit і Pest у проєкті немає.
Питання зі співбесід
Unit-тест запускає код і перевіряє результат. Архітектурний тест код не запускає: він читає файли й перевіряє будову, тобто хто кого імпортує, чи клас final, чи є strict_types. Поведінка може бути правильною, а межа шарів уже порушеною, і саме це він ловить.
not->toUse це чорний список: заборонено Illuminate, решту можна. toOnlyUse це білий список: можна лише перелічене, а будь-яка нова залежність робить тест червоним. Білий список строгіший і краще переживає одруківки, зате його доводиться оновлювати щоразу, коли шар законно отримує нову залежність.
now() це глобальна функція, у неї немає простору імен Illuminate, тож префікс не збігається. Pest рахує виклики функцій залежностями, тому досить додати їх у список: ->not->toUse(['Illuminate', 'now', 'config']). А краще передавати час через інтерфейс Clock, тоді й правило, і тест домену простіші.
Не чекати, доки все виправлять. Правило вмикається одразу, а наявні порушники йдуть у ->ignoring([...]) явним списком. Тоді нові класи перевіряються з першого дня, а список винятків видно в рев'ю, і його скорочують разом з рефакторингом. Те саме робить baseline у PHPStan для типів.