Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/conventions/core-patterns/dto.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ description: Правила создания и использования об
[Query DTO](../layers/presentation/query-dto.md),
[Response DTO](../layers/presentation/response-dto.md).
- Именование: суффикс `Dto`. Контекст — в имени/namespace (`RequestDto`, `ResponseDto`, `ResultDto`, `...Dto`).
- Если доменной операции нужен DTO, размещаем его рядом с классом или группой, которые его используют.

## Зависимости

Expand Down Expand Up @@ -79,12 +80,21 @@ description: Правила создания и использования об
{ProjectName}\Common\Module\{ModuleName}\Infrastructure\Component\{Component}\Dto\{Name}Dto
```

- DTO доменного уровня (вход/выход конкретного Domain Service):
- DTO доменного уровня размещаем рядом с доменным объектом или группой, для которых он нужен:

```
{ProjectName}\Common\Module\{ModuleName}\Domain\{DomainObjectType}\{GroupName?}\{Name}Dto
```

Например, для результата доменного сервиса:

```
{ProjectName}\Common\Module\{ModuleName}\Domain\Service\{ServiceName}\{Name}Dto
```

Общие `Domain\Dto\*` не используем. `{DomainObjectType}` — тип доменного объекта: `Service`, `Calculator`, `Specification` и т.п. Если DTO начинает
переиспользоваться за пределами одной группы, заменяем его на `VO`.

## Как используем

- В Application `Use Case` принимает/возвращает DTO, маппит из/в доменные модели через мапперы.
Expand All @@ -96,7 +106,8 @@ description: Правила создания и использования об

- Для передачи данных между слоями и границами подсистем.
- В Application разрешено общее DTO, если один и тот же набор данных переиспользуется в нескольких Use Case.
- В Domain используем DTO, когда нужно принять/вернуть данные конкретного сервиса. Если DTO начинает «гулять» по коду, рассмотрите перенос в Value Object (VO).
- В Domain используем DTO только для компактного входа/выхода конкретной доменной операции. Если DTO начинает «гулять» по коду,
рассмотрите перенос в Value Object (VO).

## Пример

Expand Down Expand Up @@ -187,6 +198,7 @@ final readonly class InferenceRequestDto
- [ ] Нет зависимостей на сервисы/репозитории/компоненты и внешнее окружение.
- [ ] Коллекции типизированы через PHPDoc (`@var FooDto[]`).
- [ ] Название и namespace отражают контекст использования (`Request/Response/Result` при необходимости).
- [ ] Domain DTO лежит рядом с доменным объектом или группой (`Domain\{DomainObjectType}\...`); классов в `Domain\Dto\*` нет.
- [ ] Денежные/точные величины представлены `numeric-string` или VO.
- [ ] DTO используется на границах слоя, а не подменяет доменные сущности.
- [ ] Для Presentation transport DTO дополнительно соблюдены профильные presentation-conventions.
7 changes: 3 additions & 4 deletions docs/conventions/core-patterns/factory.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: Правила создания и использования фа
## Общие правила

- В названии класса указывается постфикс `Factory`.
- Фабрика возвращает объекты (Entity, VO, DTO).
- Фабрика возвращает объекты ([Entity](../layers/domain/entity.md), [VO](value-object.md), [DTO](dto.md)).
- Массивы запрещены, допускаются только строго типизированные коллекции (iterable<T>, Collection<T>) с явным интерфейсом.
- Фабрика не должна содержать бизнес-логику домена — только логику создания.
- Фабрика не должна изменять состояние базы данных или выполнять I/O-операции.
Expand All @@ -21,8 +21,7 @@ description: Правила создания и использования фа
## Зависимости

**Разрешено**:
- Примитивы (только конфигурационные), Enum, VO своего модуля.
- DTO — только в Application/Presentation, не в Domain.
- Примитивы (только конфигурационные), [Enum](enum.md), [VO](value-object.md), [DTO](dto.md) своего модуля.
- Через DI Спецификации, другие фабрики своего модуля.

**Запрещено**:
Expand All @@ -42,7 +41,7 @@ description: Правила создания и использования фа
{ProjectName}\Common\Module\{ModuleName}\Domain\Factory\{Name}Factory
```

**Application слой** — фабрики для DTO и объектов use case:
**Application слой** — фабрики для [DTO](dto.md) и объектов use case:

```
{ProjectName}\Common\Module\{ModuleName}\Application\Factory\{Name}Factory
Expand Down
12 changes: 6 additions & 6 deletions docs/conventions/core-patterns/service.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,14 @@ description: Правила создания и использования се
## Общие правила

- Используется только внутри слоя **Domain**.
- Работает только с объектами домена: `Entity`, `VO`, `Enum`, `UuidInterface`, примитивы.
- Для входа/выхода предпочтительны доменные `VO` (включая именованные result/input объекты), если есть инварианты и доменный смысл.
- `DTO` в Domain допустим как технический carrier в узком месте, но не как замена доменных типов.
- Работает только с объектами домена: [Entity](../layers/domain/entity.md), [VO](value-object.md), [Enum](enum.md), `UuidInterface`, примитивы.
- Для входа/выхода предпочтительны доменные [VO](value-object.md) (включая именованные result/input объекты), если есть инварианты и доменный смысл.
- [DTO](dto.md) допустим только как простая структура входа/выхода.
- Может зависеть от:
- других сервисов своего модуля;
- `Specification`;
- `Calculator`;
- репозиториев;
- [Specification](../layers/domain/specification.md);
- [Calculator](../layers/domain/calculator.md);
- [Repository](../layers/domain/repository.md);
- общих компонентов.
- ❗ **Запрещено** зависеть от внешней инфраструктуры.

Expand Down
24 changes: 12 additions & 12 deletions docs/conventions/layers/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ description: Правила зависимостей между слоями а

- Зависимости направлены **только внутрь**, к центру
- Внутренние слои не зависят от внешних
- Внешние слои зависят от внутренних через контракты и согласованные типы (`interface`/`DTO`/`VO`/`Enum`) в рамках разрешённых правил
- Внешние слои зависят от внутренних через контракты (`interface`) и согласованные типы ([DTO](../core-patterns/dto.md), [VO](../core-patterns/value-object.md), [Enum](../core-patterns/enum.md)) в рамках разрешённых правил
- DI-контейнер связывает интерфейсы с реализациями на уровне конфигурации

## Диаграмма зависимостей
Expand Down Expand Up @@ -46,8 +46,8 @@ flowchart TB

| Слой | Назначение | Зависимости |
|------|------------|-------------|
| **Domain** | Бизнес-логика, сущности, Value Object, интерфейсы репозиториев | Нет |
| **Application** | Use Cases, оркестрация, DTO | Domain |
| **Domain** | Бизнес-логика, [Entity](domain/entity.md), [VO](../core-patterns/value-object.md), интерфейсы [Repository](domain/repository.md) | Нет |
| **Application** | Use Cases, оркестрация, [DTO](../core-patterns/dto.md) | Domain |
| **Infrastructure** | Реализация репозиториев, кэш, персистентность | Domain (контракты и типы) |
| **Integration** | Внешние API, события, межмодульное взаимодействие | Application, Domain (контракты и типы) |
| **Presentation** | Web, API, Console, Blog — точки входа | Application |
Expand All @@ -63,25 +63,25 @@ Domain слой не зависит ни от кого:
### Application → Domain

Application зависит только от Domain:
- Вызывает методы сущностей и Value Object
- Использует интерфейсы репозиториев из Domain
- Использует спецификации и сервисы из Domain
- Важно: `Application DTO` не должны зависеть от `Domain` и остаются только в рамках `Application`.
- Вызывает методы [Entity](domain/entity.md) и [VO](../core-patterns/value-object.md)
- Использует интерфейсы [Repository](domain/repository.md) из Domain
- Использует [Specification](domain/specification.md) и [Service](../core-patterns/service.md) из Domain
- Важно: [Application DTO](../core-patterns/dto.md) не должны зависеть от `Domain` и остаются только в рамках `Application`.

### Infrastructure → Domain

Infrastructure реализует интерфейсы Domain:
- Реализует `RepositoryInterface` из Domain
- Может использовать доменные типы (`VO`, `Enum`, доменные `DTO`) в сигнатурах и маппинге
- Реализует доменный `RepositoryInterface` ([Repository](domain/repository.md))
- Может использовать доменные типы ([VO](../core-patterns/value-object.md), [Enum](../core-patterns/enum.md), [DTO](../core-patterns/dto.md)) в сигнатурах и маппинге
- Подключается через DI-контейнер
- Не используется напрямую из Application

### Integration → Application

Integration вызывает Application чужого модуля:
- Обрабатывает внешние события и инициирует соответствующие Use Cases.
- Реализует интеграции через интерфейсы сервисов своего Domain.
- Может использовать доменные типы в сигнатурах контрактов (`VO`, `Enum`, доменные `DTO`).
- Реализует интеграции через интерфейсы доменных [Service](../core-patterns/service.md).
- Может использовать доменные типы в сигнатурах контрактов ([VO](../core-patterns/value-object.md), [Enum](../core-patterns/enum.md), [DTO](../core-patterns/dto.md)).
- Не зависит от слоя Infrastructure.
- Формирует антикоррупционный слой (ACL) на границе с внешними системами.

Expand All @@ -102,7 +102,7 @@ Presentation зависит только от Application:
| **Integration** | ✅* | ✅ | ❌ | — | ❌ |
| **Presentation** | ❌ | ✅ | ❌ | ❌ | — |

\* Только контракты и типы Domain (интерфейсы сервисов, `VO`/`Enum`/доменные `DTO` в сигнатурах), без зависимости на доменные реализации.
\* Только контракты и типы Domain (интерфейсы доменных [Service](../core-patterns/service.md), [VO](../core-patterns/value-object.md), [Enum](../core-patterns/enum.md), [DTO](../core-patterns/dto.md) в сигнатурах), без зависимости на доменные реализации.

## Расположение

Expand Down
23 changes: 18 additions & 5 deletions src/Deptrac/CrossModuleDomainRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
*
* All other cross-module dependencies are violations, regardless of layer.
* Shared data types (ValueObject, Enum, Dto) are excluded — safe to use across modules.
* Domain DTOs are shared only when they are scoped to an owning Domain context,
* not placed in a common Domain\Dto directory.
* Domain\Entity → foreign Domain\Entity is excluded — Doctrine ORM relations require class references.
* Infrastructure\*Criteria\Mapper → foreign Domain\Entity is excluded — query building requires entity references.
*
Expand Down Expand Up @@ -57,7 +59,7 @@ public function onProcessEvent(ProcessEvent $event): void
return;
}

if ($this->isSharedDataType($dependent['path'])) {
if ($this->isSharedDataType($dependent['layer'], $dependent['path'])) {
return;
}

Expand Down Expand Up @@ -140,12 +142,23 @@ private function parseModuleClass(string $className): ?array

/**
* ValueObject, Enum and Dto are shared data types — safe for cross-module usage.
* Domain DTOs must not be placed in a common Domain\Dto directory.
*/
private function isSharedDataType(string $path): bool
private function isSharedDataType(string $layer, string $path): bool
{
return str_starts_with($path, 'ValueObject\\')
|| str_starts_with($path, 'Enum\\')
|| (bool) preg_match('/Dto$/', $path);
if (str_starts_with($path, 'ValueObject\\') || str_starts_with($path, 'Enum\\')) {
return true;
}

if ((bool) preg_match('/Dto$/', $path) === false) {
return false;
}

if ($layer !== 'Domain') {
return true;
}

return str_starts_with($path, 'Dto\\') === false;
}

private function dependentLayerName(ProcessEvent $event): string
Expand Down
10 changes: 5 additions & 5 deletions src/Sniffs/Structure/DtoStructureSniff.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public function register(): array

public function process(File $phpcsFile, $stackPtr): void
{
$this->assertNoDomainDtoDirectory($phpcsFile, $stackPtr);

$className = $phpcsFile->getDeclarationName($stackPtr);
if ($className === '' || str_ends_with($className, 'Dto') === false) {
return;
Expand All @@ -37,8 +39,6 @@ public function process(File $phpcsFile, $stackPtr): void
return;
}

$this->assertDomainDtoPath($phpcsFile, $stackPtr);

$this->assertFinalReadonly($phpcsFile, $stackPtr);

$scopeStart = $tokens[$stackPtr]['scope_opener'];
Expand All @@ -53,7 +53,7 @@ public function process(File $phpcsFile, $stackPtr): void
$this->assertNoMembers($phpcsFile, $stackPtr, $scopeStart, $scopeEnd);
}

private function assertDomainDtoPath(File $phpcsFile, int $classPtr): void
private function assertNoDomainDtoDirectory(File $phpcsFile, int $classPtr): void
{
$normalizedPath = str_replace('\\', '/', $phpcsFile->getFilename());

Expand All @@ -62,8 +62,8 @@ private function assertDomainDtoPath(File $phpcsFile, int $classPtr): void
}

$phpcsFile->addError(
'Domain DTOs must be placed inside a Service context:'
. ' Domain/Service/{ServiceName}/{Name}Dto, not Domain/Dto/{Name}Dto.' . self::DOC_REF,
'Classes must not be placed inside Domain/Dto.'
. ' Domain DTOs must be placed next to their owning Domain context.' . self::DOC_REF,
$classPtr,
self::ERROR_DOMAIN_DTO_PATH,
);
Expand Down
16 changes: 16 additions & 0 deletions tests/Deptrac/CrossModuleDomainRuleTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,22 @@ public function testInfrastructureUsesForeignDomainEnum(): void
public function testInfrastructureUsesForeignDomainDto(): void
{
$this->assertNoViolation(
'App\Common\Module\DynamicLoop\Infrastructure\Service\SomeService',
'App\Common\Module\ChainExecution\Domain\Service\ChainResult\ChainResultAuditDto',
);
}

public function testInfrastructureUsesForeignDomainDtoOutsideServiceContext(): void
{
$this->assertNoViolation(
'App\Common\Module\DynamicLoop\Infrastructure\Service\SomeService',
'App\Common\Module\ChainExecution\Domain\Calculator\Pricing\PricingResultDto',
);
}

public function testInfrastructureUsesForeignDomainDtoFromCommonDtoDirectory(): void
{
$this->assertViolation(
'App\Common\Module\DynamicLoop\Infrastructure\Service\SomeService',
'App\Common\Module\ChainExecution\Domain\Dto\ChainResultAuditDto',
);
Expand Down
16 changes: 15 additions & 1 deletion tests/fixtures.php
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,26 @@
],
'warnings' => [],
],
// DtoStructureSniff — Domain DTO in Domain/Service/{GroupName}/ — correct path
// DtoStructureSniff — any class in Domain/Dto/ — wrong path
[
'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Dto/WrongPathPayload.inc',
'errors' => [
6 => 1,
],
'warnings' => [],
],
// DtoStructureSniff — Domain DTO in Domain/Service/{GroupName}/ — example of a correct contextual path
[
'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Service/Payment/InitPaymentResultDto.inc',
'errors' => [],
'warnings' => [],
],
// DtoStructureSniff — Domain DTO outside Domain/Dto/ — correct contextual path
[
'file' => __DIR__ . '/fixtures/src/Module/Example/Domain/Calculator/Pricing/PricingResultDto.inc',
'errors' => [],
'warnings' => [],
],
// ServiceStructureSniff — Mapper class in Service/ directory — valid per convention
[
'file' => __DIR__ . '/fixtures/src/Module/Example/Infrastructure/Service/PublicAgentSession/SessionPayloadMapper.inc',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

// Domain DTO outside Domain/Dto/ — correct contextual path.
namespace App\Module\Example\Domain\Calculator\Pricing;

final readonly class PricingResultDto
{
public function __construct(
public int $amount,
) {
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

// Domain DTO in Domain/Dto/ — wrong path, should be Domain/Service/{GroupName}/
// Domain DTO in Domain/Dto/ — wrong path: Domain/Dto/ is forbidden
namespace App\Module\Example\Domain\Dto;

final readonly class WrongPathDto
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

// Any class in Domain/Dto/ is wrong, even without Dto suffix.
namespace App\Module\Example\Domain\Dto;

final readonly class WrongPathPayload
{
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

// Domain DTO in Domain/Service/{GroupName}/ — correct path
// Domain DTO in Domain/Service/{GroupName}/ — example of a correct contextual path
namespace App\Module\Example\Domain\Service\Payment;

final readonly class InitPaymentResultDto
Expand Down
Loading