From f608b9c8fc20531630a1de9fc72df583d965e22d Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 09:18:37 +0700 Subject: [PATCH 01/10] fix: forbid layer names in domain service contexts --- docs/conventions/core-patterns/service.md | 7 +++ docs/conventions/layers/domain.md | 8 +-- docs/conventions/layers/integration.md | 2 +- .../Structure/ServiceStructureSniff.php | 53 ++++++++++++++++++- tests/fixtures.php | 8 +++ ...onnectivityRoleTargetProviderInterface.inc | 8 +++ 6 files changed, 81 insertions(+), 5 deletions(-) create mode 100644 tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc diff --git a/docs/conventions/core-patterns/service.md b/docs/conventions/core-patterns/service.md index e0c5d0b..ff86ac5 100644 --- a/docs/conventions/core-patterns/service.md +++ b/docs/conventions/core-patterns/service.md @@ -60,6 +60,9 @@ description: Правила создания и использования се {ProjectName}\Common\Module\{ModuleName}\Domain\Service\{Context?}\{ServiceName}Service ``` +- `{Context}` — бизнес-контекст сервиса, не название слоя. +- Запрещены контексты `Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation` внутри `Domain\Service`. + ## Как используем - Вызывается внутри Use Case или другого доменного сервиса. @@ -96,6 +99,8 @@ description: Правила создания и использования се {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Service\{Context?}\{ServiceName}Service ``` +- `{Context}` у интерфейса в `Domain\Service` — бизнес-контекст, не `Infrastructure`. + ## Как используем - Внедряется в Use Case или доменные сервисы (если интерфейс расположен в Domain). @@ -163,6 +168,8 @@ description: Правила создания и использования се {ProjectName}\Common\Module\{ModuleName}\Integration\Service\{Context?}\{ServiceName}Service ``` +- `{Context}` у интерфейса в `Domain\Service` — бизнес-контекст, не `Integration`. + ## Как используем - Внедряется через DI в Use Case, доменные или инфраструктурные сервисы. diff --git a/docs/conventions/layers/domain.md b/docs/conventions/layers/domain.md index 78ea432..c79c882 100644 --- a/docs/conventions/layers/domain.md +++ b/docs/conventions/layers/domain.md @@ -27,14 +27,17 @@ src/Module/{ModuleName}/Domain/ │ ├── {EntityName}RepositoryInterface.php │ └── Criteria/ ├── Service/ -│ └── Integration/ -│ └── {ServiceName}Interface.php +│ └── {Context}/ +│ └── {ServiceName}ServiceInterface.php ├── Specification/ │ └── {SpecificationName}.php └── Calculator/ └── {CalculatorName}.php ``` +- `{Context}` — предметная область или сценарий, а не название слоя. +- Не используем в `Domain\Service\...` сегменты `Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation`. + ## Чек-лист для проведения ревью кода - [ ] Domain не зависит от других слоёв. @@ -42,4 +45,3 @@ src/Module/{ModuleName}/Domain/ - [ ] Репозитории — интерфейсы, а не реализации. - [ ] Сущности проверяют инварианты. - [ ] VO неизменяемы и инкапсулируют валидацию. - diff --git a/docs/conventions/layers/integration.md b/docs/conventions/layers/integration.md index 2145ee3..c3460e7 100644 --- a/docs/conventions/layers/integration.md +++ b/docs/conventions/layers/integration.md @@ -52,6 +52,7 @@ Integration слой отвечает за межмодульное взаимо - Реализует Domain Service-интерфейсы, когда реализация связывает модули или адаптирует внешний transport. - Application оркестрирует Domain через интерфейсы, не зная, где находится реализация — в Domain, Infrastructure или Integration. +- Интерфейс остаётся в `Domain\Service\{BusinessContext}`, без сегмента `Integration` в namespace Domain. ### Listener @@ -75,4 +76,3 @@ Integration слой отвечает за межмодульное взаимо - [ ] Middleware адаптирует транспортный контекст, не реализуя бизнес-правила. - [ ] Нет прямых вызовов к Domain/Infrastructure из Listener. - [ ] Межмодульное взаимодействие идёт через Application-контракты. - diff --git a/src/Sniffs/Structure/ServiceStructureSniff.php b/src/Sniffs/Structure/ServiceStructureSniff.php index f5326ef..76261d1 100644 --- a/src/Sniffs/Structure/ServiceStructureSniff.php +++ b/src/Sniffs/Structure/ServiceStructureSniff.php @@ -13,12 +13,20 @@ final class ServiceStructureSniff implements Sniff private const ERROR_NO_INTERFACE = 'NoInterface'; private const ERROR_SERVICE_OUTSIDE_SERVICE_DIR = 'ServiceOutsideServiceDirectory'; private const ERROR_DOMAIN_SERVICE_IMPL_OUTSIDE_SERVICE_DIR = 'DomainServiceImplOutsideServiceDirectory'; + private const ERROR_DOMAIN_SERVICE_LAYER_CONTEXT = 'DomainServiceLayerContext'; private const DOC_REF = ' See: docs/conventions/core-patterns/service.md'; + private const LAYER_CONTEXT_NAMES = [ + 'Domain', + 'Application', + 'Infrastructure', + 'Integration', + 'Presentation', + ]; public function register(): array { - return [T_CLASS]; + return [T_CLASS, T_INTERFACE]; } public function process(File $phpcsFile, $stackPtr): void @@ -35,6 +43,12 @@ public function process(File $phpcsFile, $stackPtr): void return; } + $this->assertDomainServiceContextIsNotLayerName($phpcsFile, $stackPtr, $relativePath); + + if ($this->isInterfaceDeclaration($phpcsFile, $stackPtr)) { + return; + } + $inServiceDir = $this->isInServiceDirectory($relativePath); $hasSuffix = str_ends_with($className, 'Service'); @@ -73,6 +87,43 @@ private function assertImplementsInterface(File $phpcsFile, int $classPtr, strin } } + private function assertDomainServiceContextIsNotLayerName( + File $phpcsFile, + int $classPtr, + string $relativePath, + ): void { + if ( + preg_match( + '~^src/Module/[^/]+/Domain/Service/(?P[^/]+)/~', + $relativePath, + $matches, + ) !== 1 + ) { + return; + } + + $context = $matches['context']; + if (in_array($context, self::LAYER_CONTEXT_NAMES, true) === false) { + return; + } + + $phpcsFile->addError( + sprintf( + 'Domain Service context "%s" must describe business context, not a layer name.' + . ' Use Domain/Service/{BusinessContext}/..., not Domain/Service/%s/....' . self::DOC_REF, + $context, + $context, + ), + $classPtr, + self::ERROR_DOMAIN_SERVICE_LAYER_CONTEXT, + ); + } + + private function isInterfaceDeclaration(File $phpcsFile, int $stackPtr): bool + { + return $phpcsFile->getTokensAsString($stackPtr, 1) === 'interface'; + } + private function assertCompanionClassAllowed( File $phpcsFile, int $classPtr, diff --git a/tests/fixtures.php b/tests/fixtures.php index de0fe22..53eddef 100644 --- a/tests/fixtures.php +++ b/tests/fixtures.php @@ -243,6 +243,14 @@ 'errors' => [], 'warnings' => [], ], + // ServiceStructureSniff — Domain Service context must not be a layer name + [ + 'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc', + 'errors' => [ + 6 => 1, // DomainServiceLayerContext + ], + 'warnings' => [], + ], // ServiceStructureSniff — companion class with valid Service nearby [ 'file' => __DIR__ . '/fixtures/src/Module/Example/Service/WithValidService/SomeHelper.inc', diff --git a/tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc b/tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc new file mode 100644 index 0000000..55a83be --- /dev/null +++ b/tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc @@ -0,0 +1,8 @@ + Date: Thu, 4 Jun 2026 09:44:13 +0700 Subject: [PATCH 02/10] fix: clarify domain service grouping terminology --- docs/conventions/core-patterns/service.md | 7 ------ docs/conventions/layers/domain.md | 5 ++-- docs/conventions/layers/integration.md | 2 +- .../Structure/ServiceStructureSniff.php | 24 +++++++++---------- tests/fixtures.php | 4 ++-- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/docs/conventions/core-patterns/service.md b/docs/conventions/core-patterns/service.md index ff86ac5..e0c5d0b 100644 --- a/docs/conventions/core-patterns/service.md +++ b/docs/conventions/core-patterns/service.md @@ -60,9 +60,6 @@ description: Правила создания и использования се {ProjectName}\Common\Module\{ModuleName}\Domain\Service\{Context?}\{ServiceName}Service ``` -- `{Context}` — бизнес-контекст сервиса, не название слоя. -- Запрещены контексты `Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation` внутри `Domain\Service`. - ## Как используем - Вызывается внутри Use Case или другого доменного сервиса. @@ -99,8 +96,6 @@ description: Правила создания и использования се {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Service\{Context?}\{ServiceName}Service ``` -- `{Context}` у интерфейса в `Domain\Service` — бизнес-контекст, не `Infrastructure`. - ## Как используем - Внедряется в Use Case или доменные сервисы (если интерфейс расположен в Domain). @@ -168,8 +163,6 @@ description: Правила создания и использования се {ProjectName}\Common\Module\{ModuleName}\Integration\Service\{Context?}\{ServiceName}Service ``` -- `{Context}` у интерфейса в `Domain\Service` — бизнес-контекст, не `Integration`. - ## Как используем - Внедряется через DI в Use Case, доменные или инфраструктурные сервисы. diff --git a/docs/conventions/layers/domain.md b/docs/conventions/layers/domain.md index c79c882..6879cd7 100644 --- a/docs/conventions/layers/domain.md +++ b/docs/conventions/layers/domain.md @@ -27,7 +27,7 @@ src/Module/{ModuleName}/Domain/ │ ├── {EntityName}RepositoryInterface.php │ └── Criteria/ ├── Service/ -│ └── {Context}/ +│ └── {DomainArea?}/ │ └── {ServiceName}ServiceInterface.php ├── Specification/ │ └── {SpecificationName}.php @@ -35,7 +35,8 @@ src/Module/{ModuleName}/Domain/ └── {CalculatorName}.php ``` -- `{Context}` — предметная область или сценарий, а не название слоя. +- `{DomainArea?}` — опциональная группировка по предметной области или бизнес-возможности. +- Не называем эту группу «контекстом» (Context): в DDD контекст обычно означает ограниченный контекст (Bounded Context), а не папку внутри сервиса. - Не используем в `Domain\Service\...` сегменты `Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation`. ## Чек-лист для проведения ревью кода diff --git a/docs/conventions/layers/integration.md b/docs/conventions/layers/integration.md index c3460e7..e2d79ae 100644 --- a/docs/conventions/layers/integration.md +++ b/docs/conventions/layers/integration.md @@ -52,7 +52,7 @@ Integration слой отвечает за межмодульное взаимо - Реализует Domain Service-интерфейсы, когда реализация связывает модули или адаптирует внешний transport. - Application оркестрирует Domain через интерфейсы, не зная, где находится реализация — в Domain, Infrastructure или Integration. -- Интерфейс остаётся в `Domain\Service\{BusinessContext}`, без сегмента `Integration` в namespace Domain. +- Интерфейс остаётся в `Domain\Service\{DomainArea?}`, без сегмента `Integration` в namespace Domain. ### Listener diff --git a/src/Sniffs/Structure/ServiceStructureSniff.php b/src/Sniffs/Structure/ServiceStructureSniff.php index 76261d1..7cf9e95 100644 --- a/src/Sniffs/Structure/ServiceStructureSniff.php +++ b/src/Sniffs/Structure/ServiceStructureSniff.php @@ -13,10 +13,10 @@ final class ServiceStructureSniff implements Sniff private const ERROR_NO_INTERFACE = 'NoInterface'; private const ERROR_SERVICE_OUTSIDE_SERVICE_DIR = 'ServiceOutsideServiceDirectory'; private const ERROR_DOMAIN_SERVICE_IMPL_OUTSIDE_SERVICE_DIR = 'DomainServiceImplOutsideServiceDirectory'; - private const ERROR_DOMAIN_SERVICE_LAYER_CONTEXT = 'DomainServiceLayerContext'; + private const ERROR_DOMAIN_SERVICE_LAYER_SEGMENT = 'DomainServiceLayerSegment'; private const DOC_REF = ' See: docs/conventions/core-patterns/service.md'; - private const LAYER_CONTEXT_NAMES = [ + private const LAYER_NAMES = [ 'Domain', 'Application', 'Infrastructure', @@ -43,7 +43,7 @@ public function process(File $phpcsFile, $stackPtr): void return; } - $this->assertDomainServiceContextIsNotLayerName($phpcsFile, $stackPtr, $relativePath); + $this->assertDomainServicePathSegmentIsNotLayerName($phpcsFile, $stackPtr, $relativePath); if ($this->isInterfaceDeclaration($phpcsFile, $stackPtr)) { return; @@ -87,14 +87,14 @@ private function assertImplementsInterface(File $phpcsFile, int $classPtr, strin } } - private function assertDomainServiceContextIsNotLayerName( + private function assertDomainServicePathSegmentIsNotLayerName( File $phpcsFile, int $classPtr, string $relativePath, ): void { if ( preg_match( - '~^src/Module/[^/]+/Domain/Service/(?P[^/]+)/~', + '~^src/Module/[^/]+/Domain/Service/(?P[^/]+)/~', $relativePath, $matches, ) !== 1 @@ -102,20 +102,20 @@ private function assertDomainServiceContextIsNotLayerName( return; } - $context = $matches['context']; - if (in_array($context, self::LAYER_CONTEXT_NAMES, true) === false) { + $segment = $matches['segment']; + if (in_array($segment, self::LAYER_NAMES, true) === false) { return; } $phpcsFile->addError( sprintf( - 'Domain Service context "%s" must describe business context, not a layer name.' - . ' Use Domain/Service/{BusinessContext}/..., not Domain/Service/%s/....' . self::DOC_REF, - $context, - $context, + 'Domain Service path segment "%s" must describe domain area, not a layer name.' + . ' Use Domain/Service/{DomainArea?}/..., not Domain/Service/%s/....' . self::DOC_REF, + $segment, + $segment, ), $classPtr, - self::ERROR_DOMAIN_SERVICE_LAYER_CONTEXT, + self::ERROR_DOMAIN_SERVICE_LAYER_SEGMENT, ); } diff --git a/tests/fixtures.php b/tests/fixtures.php index 53eddef..6a522e0 100644 --- a/tests/fixtures.php +++ b/tests/fixtures.php @@ -243,11 +243,11 @@ 'errors' => [], 'warnings' => [], ], - // ServiceStructureSniff — Domain Service context must not be a layer name + // ServiceStructureSniff — Domain Service path segment must not be a layer name [ 'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc', 'errors' => [ - 6 => 1, // DomainServiceLayerContext + 6 => 1, // DomainServiceLayerSegment ], 'warnings' => [], ], From 909c76cc44cf64f439b97ccc6f8536a2d67a790d Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 09:48:07 +0700 Subject: [PATCH 03/10] fix: use group name for domain service grouping --- docs/conventions/layers/domain.md | 4 ++-- docs/conventions/layers/integration.md | 2 +- src/Sniffs/Structure/ServiceStructureSniff.php | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/conventions/layers/domain.md b/docs/conventions/layers/domain.md index 6879cd7..6c944c0 100644 --- a/docs/conventions/layers/domain.md +++ b/docs/conventions/layers/domain.md @@ -27,7 +27,7 @@ src/Module/{ModuleName}/Domain/ │ ├── {EntityName}RepositoryInterface.php │ └── Criteria/ ├── Service/ -│ └── {DomainArea?}/ +│ └── {GroupName?}/ │ └── {ServiceName}ServiceInterface.php ├── Specification/ │ └── {SpecificationName}.php @@ -35,7 +35,7 @@ src/Module/{ModuleName}/Domain/ └── {CalculatorName}.php ``` -- `{DomainArea?}` — опциональная группировка по предметной области или бизнес-возможности. +- `{GroupName?}` — опциональная группировка по предметной области или бизнес-возможности. - Не называем эту группу «контекстом» (Context): в DDD контекст обычно означает ограниченный контекст (Bounded Context), а не папку внутри сервиса. - Не используем в `Domain\Service\...` сегменты `Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation`. diff --git a/docs/conventions/layers/integration.md b/docs/conventions/layers/integration.md index e2d79ae..8091b53 100644 --- a/docs/conventions/layers/integration.md +++ b/docs/conventions/layers/integration.md @@ -52,7 +52,7 @@ Integration слой отвечает за межмодульное взаимо - Реализует Domain Service-интерфейсы, когда реализация связывает модули или адаптирует внешний transport. - Application оркестрирует Domain через интерфейсы, не зная, где находится реализация — в Domain, Infrastructure или Integration. -- Интерфейс остаётся в `Domain\Service\{DomainArea?}`, без сегмента `Integration` в namespace Domain. +- Интерфейс остаётся в `Domain\Service\{GroupName?}`, без сегмента `Integration` в namespace Domain. ### Listener diff --git a/src/Sniffs/Structure/ServiceStructureSniff.php b/src/Sniffs/Structure/ServiceStructureSniff.php index 7cf9e95..879c512 100644 --- a/src/Sniffs/Structure/ServiceStructureSniff.php +++ b/src/Sniffs/Structure/ServiceStructureSniff.php @@ -109,8 +109,8 @@ private function assertDomainServicePathSegmentIsNotLayerName( $phpcsFile->addError( sprintf( - 'Domain Service path segment "%s" must describe domain area, not a layer name.' - . ' Use Domain/Service/{DomainArea?}/..., not Domain/Service/%s/....' . self::DOC_REF, + 'Domain Service path segment "%s" must describe a group, not a layer name.' + . ' Use Domain/Service/{GroupName?}/..., not Domain/Service/%s/....' . self::DOC_REF, $segment, $segment, ), From 88ffe2e8bc17ddeeaee1c4710b2c58dd3fe28b8c Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 09:51:23 +0700 Subject: [PATCH 04/10] docs: standardize group placeholders --- AGENTS.md | 2 +- docs/conventions/core-patterns/helper.md | 4 ++-- docs/conventions/core-patterns/service.md | 16 ++++++++-------- docs/conventions/layers/application/event.md | 4 ++-- docs/conventions/layers/domain/criteria.md | 6 +++--- docs/conventions/layers/domain/entity.md | 2 +- docs/conventions/layers/domain/specification.md | 4 ++-- src/Sniffs/Structure/ServiceStructureSniff.php | 4 ++-- tests/fixtures.php | 2 +- .../Module/Example/Domain/Dto/WrongPathDto.inc | 2 +- .../Service/Payment/InitPaymentResultDto.inc | 2 +- 11 files changed, 24 insertions(+), 24 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 9063c82..c4fbfdc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -114,7 +114,7 @@ description: {Описание} ## Расположение *(если применимо)* - Слой (Domain, Application, Infrastructure, Presentation). -- Путь: `{ProjectName}\Common\Module\{ModuleName}\{Layer}\{Context}\{Name}` +- Путь: `{ProjectName}\Common\Module\{ModuleName}\{Layer}\{GroupName?}\{Name}` ## Как используем diff --git a/docs/conventions/core-patterns/helper.md b/docs/conventions/core-patterns/helper.md index 94e5139..0e50cd5 100644 --- a/docs/conventions/core-patterns/helper.md +++ b/docs/conventions/core-patterns/helper.md @@ -53,12 +53,12 @@ description: Правила создания и использования хе **Infrastructure слой** — утилитные методы для инфраструктурных операций: ``` -{ProjectName}\Common\Module\{ModuleName}\Infrastructure\Service\{Context}\Helper\{Name}Helper +{ProjectName}\Common\Module\{ModuleName}\Infrastructure\Service\{GroupName}\Helper\{Name}Helper ``` ## Как используем -- Группирует утилиты одного узкого контекста. +- Группирует утилиты одной узкой группы. - Используется через статический вызов. - В DI не регистрируется. - Хелпер не должен заменять сервисы — если нужна инъекция зависимостей, создавайте сервис. diff --git a/docs/conventions/core-patterns/service.md b/docs/conventions/core-patterns/service.md index e0c5d0b..e0d5306 100644 --- a/docs/conventions/core-patterns/service.md +++ b/docs/conventions/core-patterns/service.md @@ -56,8 +56,8 @@ description: Правила создания и использования се - Интерфейс и реализация в `Domain`: ```php -{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{Context?}\{ServiceName}ServiceInterface -{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{Context?}\{ServiceName}Service +{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{GroupName?}\{ServiceName}ServiceInterface +{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{GroupName?}\{ServiceName}Service ``` ## Как используем @@ -87,13 +87,13 @@ description: Правила создания и использования се - Интерфейс: ```php -{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{Context?}\{ServiceName}ServiceInterface +{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{GroupName?}\{ServiceName}ServiceInterface ``` - Реализация: ```php -{ProjectName}\Common\Module\{ModuleName}\Infrastructure\Service\{Context?}\{ServiceName}Service +{ProjectName}\Common\Module\{ModuleName}\Infrastructure\Service\{GroupName?}\{ServiceName}Service ``` ## Как используем @@ -126,8 +126,8 @@ description: Правила создания и использования се - Интерфейс и реализация в `Application`: ```php -{ProjectName}\Common\Module\{ModuleName}\Application\Service\{Context?}\{ServiceName}ServiceInterface -{ProjectName}\Common\Module\{ModuleName}\Application\Service\{Context?}\{ServiceName}Service +{ProjectName}\Common\Module\{ModuleName}\Application\Service\{GroupName?}\{ServiceName}ServiceInterface +{ProjectName}\Common\Module\{ModuleName}\Application\Service\{GroupName?}\{ServiceName}Service ``` ## Как используем @@ -154,13 +154,13 @@ description: Правила создания и использования се - Интерфейс: ```php -{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{Context?}\{ServiceName}ServiceInterface +{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{GroupName?}\{ServiceName}ServiceInterface ``` - Реализация: ```php -{ProjectName}\Common\Module\{ModuleName}\Integration\Service\{Context?}\{ServiceName}Service +{ProjectName}\Common\Module\{ModuleName}\Integration\Service\{GroupName?}\{ServiceName}Service ``` ## Как используем diff --git a/docs/conventions/layers/application/event.md b/docs/conventions/layers/application/event.md index e197011..a0c04e0 100644 --- a/docs/conventions/layers/application/event.md +++ b/docs/conventions/layers/application/event.md @@ -28,10 +28,10 @@ description: Правила создания и использования до - В слое [Application](../application.md): ```php -{ProjectName}\Common\Module\{ModuleName}\Application\Event\{Context?}\{EventName}Event +{ProjectName}\Common\Module\{ModuleName}\Application\Event\{GroupName?}\{EventName}Event ``` -* `{Context?}` — опционально, используется для группировки по смыслу. +* `{GroupName?}` — опциональная группа событий по смыслу. * `{EventName}` — имя события без суффикса `Event`. ## Когда использовать diff --git a/docs/conventions/layers/domain/criteria.md b/docs/conventions/layers/domain/criteria.md index 0f348f2..26ec81f 100644 --- a/docs/conventions/layers/domain/criteria.md +++ b/docs/conventions/layers/domain/criteria.md @@ -13,8 +13,8 @@ description: Правила создания критериев для филь ## Общие правила -- **Структура:** интерфейс и реализации в `Domain`, имя `{Entity}{Context}Criteria`. -- **Использование:** Репозиторий принимает интерфейс критерия. Один критерий описывает только один контекст выборки. +- **Структура:** интерфейс и реализации в `Domain`, имя `{CriteriaName}Criteria`. +- **Использование:** Репозиторий принимает интерфейс критерия. Один критерий описывает только один сценарий выборки. - **Ограничения:** без бизнес-логики, только фильтры/пагинация/сортировка. - **Способ задания:** через `set*()` или конструктор, `set*()` всегда `void` и с нормализацией. - Для пагинации и сортировки применяются унифицированные интерфейсы: @@ -179,7 +179,7 @@ final class PaymentFindCriteria implements PaymentCriteriaInterface, SortableCri - [ ] Интерфейс критерия лежит в `Domain/Repository/{EntityName}`. - [ ] Реализация находится в `Domain/Repository/{EntityName}/Criteria`. -- [ ] Название соответствует паттерну `{Entity}{Context}Criteria`. +- [ ] Название соответствует паттерну `{CriteriaName}Criteria`. - [ ] В критерии нет бизнес-логики. - [ ] `set*()` возвращают `void` и нормализуют данные. - [ ] Пагинация/сортировка реализованы через унифицированные интерфейсы/трейты. diff --git a/docs/conventions/layers/domain/entity.md b/docs/conventions/layers/domain/entity.md index 89a4720..18f396d 100644 --- a/docs/conventions/layers/domain/entity.md +++ b/docs/conventions/layers/domain/entity.md @@ -57,7 +57,7 @@ description: Правила проектирования доменных сущ - В слое [Domain](../domain.md): ```php -{ProjectName}\Common\Module\{ModuleName}\Domain\Entity\{Context}\{EntityName}Model +{ProjectName}\Common\Module\{ModuleName}\Domain\Entity\{GroupName?}\{EntityName}Model ``` ## Как используем diff --git a/docs/conventions/layers/domain/specification.md b/docs/conventions/layers/domain/specification.md index 62f683e..ca41724 100644 --- a/docs/conventions/layers/domain/specification.md +++ b/docs/conventions/layers/domain/specification.md @@ -33,10 +33,10 @@ description: Правила использования паттерна Specific - В слое [Domain](../domain.md): ```php -{ProjectName}\Common\Module\{ModuleName}\Domain\Specification\{Context}\{SpecificationName}Specification +{ProjectName}\Common\Module\{ModuleName}\Domain\Specification\{GroupName?}\{SpecificationName}Specification ``` -`{Context}` используется при необходимости логически сгруппировать спецификации внутри модуля. +`{GroupName?}` используется при необходимости логически сгруппировать спецификации внутри модуля. ## Как используем diff --git a/src/Sniffs/Structure/ServiceStructureSniff.php b/src/Sniffs/Structure/ServiceStructureSniff.php index 879c512..fd270a5 100644 --- a/src/Sniffs/Structure/ServiceStructureSniff.php +++ b/src/Sniffs/Structure/ServiceStructureSniff.php @@ -163,7 +163,7 @@ private function assertServiceInCorrectDirectory( $phpcsFile->addError( sprintf( 'Service class "%s" must be placed in a Service/ directory.' - . ' Move it to .../Service/{Context?}/%s.' . self::DOC_REF, + . ' Move it to .../Service/{GroupName?}/%s.' . self::DOC_REF, $className, $className, ), @@ -190,7 +190,7 @@ private function assertDomainServiceImplInServiceDirectory( sprintf( 'Class "%s" implements Domain Service interface "%s"' . ' but is not in a Service/ directory.' - . ' Move it to .../Service/{Context?}/%s.' . self::DOC_REF, + . ' Move it to .../Service/{GroupName?}/%s.' . self::DOC_REF, $className, $fqcn, $className, diff --git a/tests/fixtures.php b/tests/fixtures.php index 6a522e0..d52c772 100644 --- a/tests/fixtures.php +++ b/tests/fixtures.php @@ -327,7 +327,7 @@ ], 'warnings' => [], ], - // DtoStructureSniff — Domain DTO in Domain/Service/{Context}/ — correct path + // DtoStructureSniff — Domain DTO in Domain/Service/{GroupName}/ — correct path [ 'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Service/Payment/InitPaymentResultDto.inc', 'errors' => [], diff --git a/tests/fixtures/src/Module/Example/Domain/Dto/WrongPathDto.inc b/tests/fixtures/src/Module/Example/Domain/Dto/WrongPathDto.inc index 0dd9267..5d12933 100644 --- a/tests/fixtures/src/Module/Example/Domain/Dto/WrongPathDto.inc +++ b/tests/fixtures/src/Module/Example/Domain/Dto/WrongPathDto.inc @@ -1,6 +1,6 @@ Date: Thu, 4 Jun 2026 09:57:24 +0700 Subject: [PATCH 05/10] docs: keep entity name in criteria names --- docs/conventions/layers/domain/criteria.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/conventions/layers/domain/criteria.md b/docs/conventions/layers/domain/criteria.md index 26ec81f..335e7ec 100644 --- a/docs/conventions/layers/domain/criteria.md +++ b/docs/conventions/layers/domain/criteria.md @@ -13,7 +13,7 @@ description: Правила создания критериев для филь ## Общие правила -- **Структура:** интерфейс и реализации в `Domain`, имя `{CriteriaName}Criteria`. +- **Структура:** интерфейс и реализации в `Domain`, имя `{EntityName}{CriteriaName}Criteria`. - **Использование:** Репозиторий принимает интерфейс критерия. Один критерий описывает только один сценарий выборки. - **Ограничения:** без бизнес-логики, только фильтры/пагинация/сортировка. - **Способ задания:** через `set*()` или конструктор, `set*()` всегда `void` и с нормализацией. @@ -37,14 +37,16 @@ description: Правила создания критериев для филь ```php namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\{EntityName}CriteriaInterface -namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Criteria\{CriteriaName}Criteria +namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Criteria\{EntityName}{CriteriaName}Criteria namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Enum\{EntityName}SortFieldEnum; // при использовании enum ``` +- `{CriteriaName}` — назначение или сценарий выборки без имени сущности и суффикса `Criteria`, например `Find`. + - Mapper в слое [Infrastructure](../infrastructure.md): ```php -namespace {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Repository\{EntityName}\Criteria\Mapper\{CriteriaName}CriteriaMapper +namespace {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Repository\{EntityName}\Criteria\Mapper\{EntityName}{CriteriaName}CriteriaMapper ``` ## Как используем @@ -179,7 +181,7 @@ final class PaymentFindCriteria implements PaymentCriteriaInterface, SortableCri - [ ] Интерфейс критерия лежит в `Domain/Repository/{EntityName}`. - [ ] Реализация находится в `Domain/Repository/{EntityName}/Criteria`. -- [ ] Название соответствует паттерну `{CriteriaName}Criteria`. +- [ ] Название соответствует паттерну `{EntityName}{CriteriaName}Criteria`. - [ ] В критерии нет бизнес-логики. - [ ] `set*()` возвращают `void` и нормализуют данные. - [ ] Пагинация/сортировка реализованы через унифицированные интерфейсы/трейты. From 68a0db2b87ee4b7b654340f7ccfacdefbddafd72 Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 10:00:28 +0700 Subject: [PATCH 06/10] docs: clarify criteria selection placeholder --- docs/conventions/layers/domain/criteria.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/conventions/layers/domain/criteria.md b/docs/conventions/layers/domain/criteria.md index 335e7ec..1b0aec6 100644 --- a/docs/conventions/layers/domain/criteria.md +++ b/docs/conventions/layers/domain/criteria.md @@ -13,7 +13,7 @@ description: Правила создания критериев для филь ## Общие правила -- **Структура:** интерфейс и реализации в `Domain`, имя `{EntityName}{CriteriaName}Criteria`. +- **Структура:** интерфейс и реализации в `Domain`, имя `{EntityName}{SelectionName}Criteria`. - **Использование:** Репозиторий принимает интерфейс критерия. Один критерий описывает только один сценарий выборки. - **Ограничения:** без бизнес-логики, только фильтры/пагинация/сортировка. - **Способ задания:** через `set*()` или конструктор, `set*()` всегда `void` и с нормализацией. @@ -37,16 +37,16 @@ description: Правила создания критериев для филь ```php namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\{EntityName}CriteriaInterface -namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Criteria\{EntityName}{CriteriaName}Criteria +namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Criteria\{EntityName}{SelectionName}Criteria namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Enum\{EntityName}SortFieldEnum; // при использовании enum ``` -- `{CriteriaName}` — назначение или сценарий выборки без имени сущности и суффикса `Criteria`, например `Find`. +- `{SelectionName}` — сценарий или назначение выборки без имени сущности и суффикса `Criteria`, например `Find`, `Active`, `ByStatus`. - Mapper в слое [Infrastructure](../infrastructure.md): ```php -namespace {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Repository\{EntityName}\Criteria\Mapper\{EntityName}{CriteriaName}CriteriaMapper +namespace {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Repository\{EntityName}\Criteria\Mapper\{EntityName}{SelectionName}CriteriaMapper ``` ## Как используем @@ -181,7 +181,7 @@ final class PaymentFindCriteria implements PaymentCriteriaInterface, SortableCri - [ ] Интерфейс критерия лежит в `Domain/Repository/{EntityName}`. - [ ] Реализация находится в `Domain/Repository/{EntityName}/Criteria`. -- [ ] Название соответствует паттерну `{EntityName}{CriteriaName}Criteria`. +- [ ] Название соответствует паттерну `{EntityName}{SelectionName}Criteria`. - [ ] В критерии нет бизнес-логики. - [ ] `set*()` возвращают `void` и нормализуют данные. - [ ] Пагинация/сортировка реализованы через унифицированные интерфейсы/трейты. From 5be1362a676ac174e77d74c2e5ed36bbe83030bb Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 10:07:58 +0700 Subject: [PATCH 07/10] docs: use search name for criteria variant --- docs/conventions/layers/domain/criteria.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/conventions/layers/domain/criteria.md b/docs/conventions/layers/domain/criteria.md index 1b0aec6..46a59f6 100644 --- a/docs/conventions/layers/domain/criteria.md +++ b/docs/conventions/layers/domain/criteria.md @@ -13,7 +13,7 @@ description: Правила создания критериев для филь ## Общие правила -- **Структура:** интерфейс и реализации в `Domain`, имя `{EntityName}{SelectionName}Criteria`. +- **Структура:** интерфейс и реализации в `Domain`, имя `{EntityName}{SearchName}Criteria`. - **Использование:** Репозиторий принимает интерфейс критерия. Один критерий описывает только один сценарий выборки. - **Ограничения:** без бизнес-логики, только фильтры/пагинация/сортировка. - **Способ задания:** через `set*()` или конструктор, `set*()` всегда `void` и с нормализацией. @@ -37,16 +37,16 @@ description: Правила создания критериев для филь ```php namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\{EntityName}CriteriaInterface -namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Criteria\{EntityName}{SelectionName}Criteria +namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Criteria\{EntityName}{SearchName}Criteria namespace {ProjectName}\Common\Module\{ModuleName}\Domain\Repository\{EntityName}\Enum\{EntityName}SortFieldEnum; // при использовании enum ``` -- `{SelectionName}` — сценарий или назначение выборки без имени сущности и суффикса `Criteria`, например `Find`, `Active`, `ByStatus`. +- `{SearchName}` — сценарий или способ поиска без имени сущности и суффикса `Criteria`, например `Find`, `Active`, `ByStatus`. - Mapper в слое [Infrastructure](../infrastructure.md): ```php -namespace {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Repository\{EntityName}\Criteria\Mapper\{EntityName}{SelectionName}CriteriaMapper +namespace {ProjectName}\Common\Module\{ModuleName}\Infrastructure\Repository\{EntityName}\Criteria\Mapper\{EntityName}{SearchName}CriteriaMapper ``` ## Как используем @@ -181,7 +181,7 @@ final class PaymentFindCriteria implements PaymentCriteriaInterface, SortableCri - [ ] Интерфейс критерия лежит в `Domain/Repository/{EntityName}`. - [ ] Реализация находится в `Domain/Repository/{EntityName}/Criteria`. -- [ ] Название соответствует паттерну `{EntityName}{SelectionName}Criteria`. +- [ ] Название соответствует паттерну `{EntityName}{SearchName}Criteria`. - [ ] В критерии нет бизнес-логики. - [ ] `set*()` возвращают `void` и нормализуют данные. - [ ] Пагинация/сортировка реализованы через унифицированные интерфейсы/трейты. From 226a2064796af2b851b6166fe4a294d6ec7d5ef9 Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 10:12:10 +0700 Subject: [PATCH 08/10] docs: simplify domain layer service layout --- docs/conventions/layers/domain.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/conventions/layers/domain.md b/docs/conventions/layers/domain.md index 6c944c0..13c7cff 100644 --- a/docs/conventions/layers/domain.md +++ b/docs/conventions/layers/domain.md @@ -27,18 +27,12 @@ src/Module/{ModuleName}/Domain/ │ ├── {EntityName}RepositoryInterface.php │ └── Criteria/ ├── Service/ -│ └── {GroupName?}/ -│ └── {ServiceName}ServiceInterface.php ├── Specification/ │ └── {SpecificationName}.php └── Calculator/ └── {CalculatorName}.php ``` -- `{GroupName?}` — опциональная группировка по предметной области или бизнес-возможности. -- Не называем эту группу «контекстом» (Context): в DDD контекст обычно означает ограниченный контекст (Bounded Context), а не папку внутри сервиса. -- Не используем в `Domain\Service\...` сегменты `Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation`. - ## Чек-лист для проведения ревью кода - [ ] Domain не зависит от других слоёв. From 3e19515e8858846fe433ab7a413112af23c13e3f Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 10:21:30 +0700 Subject: [PATCH 09/10] docs: simplify integration service notes --- docs/conventions/layers/integration.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/conventions/layers/integration.md b/docs/conventions/layers/integration.md index 8091b53..04c60c5 100644 --- a/docs/conventions/layers/integration.md +++ b/docs/conventions/layers/integration.md @@ -52,7 +52,6 @@ Integration слой отвечает за межмодульное взаимо - Реализует Domain Service-интерфейсы, когда реализация связывает модули или адаптирует внешний transport. - Application оркестрирует Domain через интерфейсы, не зная, где находится реализация — в Domain, Infrastructure или Integration. -- Интерфейс остаётся в `Domain\Service\{GroupName?}`, без сегмента `Integration` в namespace Domain. ### Listener From 2bf91631d75ff17b4e789cf832a589d3b463f7a5 Mon Sep 17 00:00:00 2001 From: Dmitry Prikotov Date: Thu, 4 Jun 2026 10:22:58 +0700 Subject: [PATCH 10/10] fix: keep service sniff scope unchanged --- .../Structure/ServiceStructureSniff.php | 53 +------------------ tests/fixtures.php | 8 --- ...onnectivityRoleTargetProviderInterface.inc | 8 --- 3 files changed, 1 insertion(+), 68 deletions(-) delete mode 100644 tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc diff --git a/src/Sniffs/Structure/ServiceStructureSniff.php b/src/Sniffs/Structure/ServiceStructureSniff.php index fd270a5..795d39a 100644 --- a/src/Sniffs/Structure/ServiceStructureSniff.php +++ b/src/Sniffs/Structure/ServiceStructureSniff.php @@ -13,20 +13,12 @@ final class ServiceStructureSniff implements Sniff private const ERROR_NO_INTERFACE = 'NoInterface'; private const ERROR_SERVICE_OUTSIDE_SERVICE_DIR = 'ServiceOutsideServiceDirectory'; private const ERROR_DOMAIN_SERVICE_IMPL_OUTSIDE_SERVICE_DIR = 'DomainServiceImplOutsideServiceDirectory'; - private const ERROR_DOMAIN_SERVICE_LAYER_SEGMENT = 'DomainServiceLayerSegment'; private const DOC_REF = ' See: docs/conventions/core-patterns/service.md'; - private const LAYER_NAMES = [ - 'Domain', - 'Application', - 'Infrastructure', - 'Integration', - 'Presentation', - ]; public function register(): array { - return [T_CLASS, T_INTERFACE]; + return [T_CLASS]; } public function process(File $phpcsFile, $stackPtr): void @@ -43,12 +35,6 @@ public function process(File $phpcsFile, $stackPtr): void return; } - $this->assertDomainServicePathSegmentIsNotLayerName($phpcsFile, $stackPtr, $relativePath); - - if ($this->isInterfaceDeclaration($phpcsFile, $stackPtr)) { - return; - } - $inServiceDir = $this->isInServiceDirectory($relativePath); $hasSuffix = str_ends_with($className, 'Service'); @@ -87,43 +73,6 @@ private function assertImplementsInterface(File $phpcsFile, int $classPtr, strin } } - private function assertDomainServicePathSegmentIsNotLayerName( - File $phpcsFile, - int $classPtr, - string $relativePath, - ): void { - if ( - preg_match( - '~^src/Module/[^/]+/Domain/Service/(?P[^/]+)/~', - $relativePath, - $matches, - ) !== 1 - ) { - return; - } - - $segment = $matches['segment']; - if (in_array($segment, self::LAYER_NAMES, true) === false) { - return; - } - - $phpcsFile->addError( - sprintf( - 'Domain Service path segment "%s" must describe a group, not a layer name.' - . ' Use Domain/Service/{GroupName?}/..., not Domain/Service/%s/....' . self::DOC_REF, - $segment, - $segment, - ), - $classPtr, - self::ERROR_DOMAIN_SERVICE_LAYER_SEGMENT, - ); - } - - private function isInterfaceDeclaration(File $phpcsFile, int $stackPtr): bool - { - return $phpcsFile->getTokensAsString($stackPtr, 1) === 'interface'; - } - private function assertCompanionClassAllowed( File $phpcsFile, int $classPtr, diff --git a/tests/fixtures.php b/tests/fixtures.php index d52c772..eb5712d 100644 --- a/tests/fixtures.php +++ b/tests/fixtures.php @@ -243,14 +243,6 @@ 'errors' => [], 'warnings' => [], ], - // ServiceStructureSniff — Domain Service path segment must not be a layer name - [ - 'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc', - 'errors' => [ - 6 => 1, // DomainServiceLayerSegment - ], - 'warnings' => [], - ], // ServiceStructureSniff — companion class with valid Service nearby [ 'file' => __DIR__ . '/fixtures/src/Module/Example/Service/WithValidService/SomeHelper.inc', diff --git a/tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc b/tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc deleted file mode 100644 index 55a83be..0000000 --- a/tests/fixtures/src/Module/Example/Domain/Service/Integration/ConnectivityRoleTargetProviderInterface.inc +++ /dev/null @@ -1,8 +0,0 @@ -