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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@
|---|---|
| `ServiceContractDependencyRule` | Infrastructure зависит только от Domain-интерфейсов, не от конкретных классов |
| `CrossModuleDomainRule` | Домен одного модуля не зависит от домена другого — только через Application DTO |
| `ReservedLayerSegmentRule` | Имена слоёв зарезервированы как сегменты namespace: вложенный слой внутри другого слоя (например, `Domain\Service\Integration\...`) — ошибка на само существование класса, независимо от зависимостей |

Готовый `depfile.yaml` с правилами для DDD-слоёв и модульных границ: [`config/deptrac/`](config/deptrac/). Копируется в проект через `coding-standard-init` или вручную.

Expand Down
1 change: 1 addition & 0 deletions bin/run-sniff-tests.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
'PrikotovCodingStandard.Structure.ServiceStructure',
'PrikotovCodingStandard.Structure.ValueObjectStructure',
'PrikotovCodingStandard.Namespaces.PresentationLayerNamespace',
'PrikotovCodingStandard.Namespaces.ReservedLayerSegment',
],
'fixtures' => require $packageRoot . '/tests/fixtures.php',
],
Expand Down
75 changes: 38 additions & 37 deletions config/deptrac/depfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,77 +25,74 @@ deptrac:
- type: bool
must:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\.*
must_not:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\(Entity\\)?ValueObject\\.*Vo$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\(Entity\\)?ValueObject\\.*Vo$
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\Specification\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\Specification\\.*
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\Enum\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\Enum\\.*
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\.*Dto$

- name: Application
collectors:
- type: bool
must:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\.*
must_not:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\.*Dto$
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\Enum\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\Enum\\.*
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Command\\.*\\.*Command$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Command\\.*\\.*Command$
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Command\\.*\\.*CommandHandler$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Command\\.*\\.*CommandHandler$
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Query\\.*\\.*Query$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Query\\.*\\.*Query$
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Query\\.*\\.*QueryHandler$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Query\\.*\\.*QueryHandler$

- name: IntegrationListener
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Integration\\Listener\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Integration\\Listener\\.*

- name: Infrastructure
collectors:
- type: bool
must:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\(?!Model|Listener).*$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\(?!Model|Listener).*$
must_not:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\.*Dto$
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\Component\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\Component\\.*
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\Enum\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\Enum\\.*

- name: InfrastructureComponent
collectors:
- type: bool
must:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\Component\\.*(Component|ComponentInterface)$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\Component\\.*(Component|ComponentInterface)$

- name: Integration
collectors:
- type: bool
must:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Integration\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Integration\\.*
must_not:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Integration\\Listener\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Integration\\Listener\\.*
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Integration\\.*Dto$
# Domain\\Service\\Integration — ports (interfaces), stay in Domain layer
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\Service\\Integration\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Integration\\.*Dto$

- name: Presentation
collectors:
Expand All @@ -108,32 +105,32 @@ deptrac:
- name: DomainSpecification
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\Specification\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\Specification\\.*

- name: DomainVo
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\(Entity\\)?ValueObject\\.*Vo$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\(Entity\\)?ValueObject\\.*Vo$

- name: DomainEnum
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\Enum\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\Enum\\.*

- name: DomainDto
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Domain\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Domain\\.*Dto$

- name: ApplicationDto
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\.*Dto$

- name: ApplicationEnum
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\Enum\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\Enum\\.*

- name: ApplicationCommonDto
collectors:
Expand All @@ -143,39 +140,39 @@ deptrac:
- name: IntegrationDto
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Integration\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Integration\\.*Dto$

- name: InfrastructureDto
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\.*Dto$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\.*Dto$

- name: InfrastructureEnum
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Infrastructure\\Enum\\.*
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Infrastructure\\Enum\\.*

# --- CQRS artifacts ---

- name: ApplicationCommand
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Command\\.*\\.*Command$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Command\\.*\\.*Command$

- name: ApplicationCommandHandler
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Command\\.*\\.*CommandHandler$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Command\\.*\\.*CommandHandler$

- name: ApplicationQuery
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Query\\.*\\.*Query$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Query\\.*\\.*Query$

- name: ApplicationQueryHandler
collectors:
- type: classLike
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\.*\\Application\\UseCase\\Query\\.*\\.*QueryHandler$
value: ^(?:[A-Za-z_][A-Za-z0-9_]*\\)?Common\\Module\\[^\\]+\\Application\\UseCase\\Query\\.*\\.*QueryHandler$

ruleset:
# DTO boundary
Expand Down Expand Up @@ -311,6 +308,10 @@ services:
autowire: true
tags:
- { name: kernel.event_subscriber }
- class: PrikotovCodingStandard\Deptrac\ReservedLayerSegmentRule
autowire: true
tags:
- { name: kernel.event_subscriber }
- class: PrikotovCodingStandard\Deptrac\CrossModuleDomainRule
autowire: true
tags:
Expand Down
2 changes: 2 additions & 0 deletions docs/conventions/layers/layers.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ description: Правила зависимостей между слоями а

- Зависимости направлены **только внутрь**, к центру
- Внутренние слои не зависят от внешних
- Имена слоёв (`Domain`, `Application`, `Infrastructure`, `Integration`, `Presentation`) зарезервированы как сегменты пути: слой — это сегмент сразу после имени модуля; вложенных повторов имён слоёв в namespace быть не должно (например, `Domain\Service\Integration\...` запрещён)
- Внешние слои зависят от внутренних через контракты (`interface`) и согласованные типы ([DTO](../core-patterns/dto.md), [VO](../core-patterns/value-object.md), [Enum](../core-patterns/enum.md)) в рамках разрешённых правил
- DI-контейнер связывает интерфейсы с реализациями на уровне конфигурации

Expand Down Expand Up @@ -130,4 +131,5 @@ Presentation зависит только от Application:
- [ ] Infrastructure реализует контракты Domain.
- [ ] Integration обращается к Domain (только контракты) и Application.
- [ ] Presentation обращается только к Application.
- [ ] Имена слоёв не используются как вложенные сегменты namespace другого слоя.
- [ ] Namespace следует паттерну `{ProjectName}\{AppGroup}\Module\...`.
123 changes: 123 additions & 0 deletions src/Deptrac/ReservedLayerSegmentRule.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

declare(strict_types=1);

namespace PrikotovCodingStandard\Deptrac;

use Qossmic\Deptrac\Contract\Analyser\PostProcessEvent;
use Qossmic\Deptrac\Contract\Analyser\ViolationCreatingInterface;
use Qossmic\Deptrac\Contract\Result\Error;
use Qossmic\Deptrac\Core\Ast\AstMapExtractor;

/**
* Custom Deptrac rule that forbids reserved layer names as nested namespace segments.
*
* Layer names (Domain, Application, Infrastructure, Integration) are reserved
* path segments: the layer is the segment right after Module\{ModuleName}.
* A namespace of one layer must not contain another layer name — for example,
* Domain\Service\Integration\*Interface is forbidden.
*
* This is the deptrac-side twin of ReservedLayerSegmentSniff: it fails the
* analysis on the mere existence of such a class, even in projects that do
* not run PHPCS. Unlike layer rulesets, it is not limited to dependency edges.
*
* Register in depfile.yaml:
* services:
* - class: PrikotovCodingStandard\Deptrac\ReservedLayerSegmentRule
* autowire: true
* tags:
* - { name: kernel.event_subscriber }
*/
final class ReservedLayerSegmentRule implements ViolationCreatingInterface
{
private const DOC_REF = ' See: docs/conventions/layers/layers.md';

/**
* Reserved layer names.
*
* @var list<string>
*/
private const LAYERS = ['Domain', 'Application', 'Infrastructure', 'Integration'];

private const MODULE_CLASS_PATTERN = '/^(?:[A-Za-z_][A-Za-z0-9_]*\\\\)?Common\\\\Module\\\\'
. '(?P<module>[A-Za-z][A-Za-z0-9]*)\\\\'
. '(?P<layer>Domain|Application|Infrastructure|Integration)\\\\'
. '(?P<path>.+)$/';

public function __construct(private readonly AstMapExtractor $astMapExtractor)
{
}

public static function getSubscribedEvents(): array
{
return [
PostProcessEvent::class => 'onPostProcessEvent',
];
}

public function onPostProcessEvent(PostProcessEvent $event): void
{
$astMap = $this->astMapExtractor->extract();

foreach ($astMap->getClassLikeReferences() as $reference) {
$className = $reference->getToken()->toString();
$nestedLayer = self::findNestedLayerName($className);

if ($nestedLayer !== null) {
$event->getResult()->addError(new Error(
sprintf(
'Class "%s" namespace contains reserved layer name "%s" inside the %s layer.'
. ' Layer names are reserved path segments — rename the group'
. ' or move the code to the %s layer.',
$className,
$nestedLayer,
self::resolveLayer($className),
$nestedLayer,
) . self::DOC_REF,
));
}
}
}

public function ruleName(): string
{
return 'ReservedLayerSegmentRule';
}

public function ruleDescription(): string
{
return 'Layer names are reserved namespace segments: a class of one layer'
. ' must not contain another layer name in its namespace;'
. ' such namespaces are forbidden regardless of dependencies.';
}

/**
* Returns the reserved layer name nested inside another layer, if any.
*/
public static function findNestedLayerName(string $className): ?string
{
if (1 !== preg_match(self::MODULE_CLASS_PATTERN, $className, $matches)) {
return null;
}

foreach (explode('\\', $matches['path']) as $segment) {
if (in_array($segment, self::LAYERS, true)) {
return $segment;
}
}

return null;
}

/**
* Returns the layer segment — the one right after Module\{ModuleName}.
*/
public static function resolveLayer(string $className): ?string
{
if (1 !== preg_match(self::MODULE_CLASS_PATTERN, $className, $matches)) {
return null;
}

return $matches['layer'];
}
}
Loading
Loading