Skip to content

Commit c497298

Browse files
author
Florian Krämer
committed
Fixing layer rules
1 parent 63f4d68 commit c497298

File tree

6 files changed

+110
-40
lines changed

6 files changed

+110
-40
lines changed

docs/Rules.md

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -104,14 +104,15 @@ Enforces strict dependency rules for modular hexagonal (Ports and Adapters) arch
104104

105105
**Note:** You can customize these layer dependencies to match your architecture needs (see configuration examples below).
106106

107-
2. **Cross-Module Dependencies** - Between different modules (default configuration):
108-
- Modules can ONLY import from other modules:
107+
2. **Cross-Module Dependencies** - Between different modules:
108+
- You must explicitly configure which classes can be imported cross-module using regex patterns
109+
- Common patterns include:
109110
- `*Facade.php` and `*FacadeInterface.php`
110111
- `*Input.php` (DTOs from UseCases)
111112
- `*Result.php` (DTOs from UseCases)
112-
- All other cross-module imports are forbidden (including exceptions, entities, value objects, etc.)
113+
- Without configured patterns, ALL cross-module imports are forbidden
113114

114-
**Note:** You can customize which classes can be imported cross-module using regex patterns (see configuration examples below).
115+
**Note:** There are no default cross-module patterns - you must explicitly configure them based on your architecture needs.
115116

116117
**Note:** For circular dependency detection between modules, use the separate `CircularModuleDependencyRule`.
117118

@@ -126,13 +127,19 @@ src/Capability/
126127
Presentation/ # Controllers, CLI commands
127128
```
128129

129-
**Configuration Example (Default):**
130+
**Configuration Example (Basic):**
130131

131132
```neon
132133
-
133134
class: Phauthentic\PHPStanRules\Architecture\ModularArchitectureRule
134135
arguments:
135136
baseNamespace: 'App\Capability'
137+
layerDependencies: null # Uses default layer rules
138+
allowedCrossModulePatterns:
139+
- '/Facade$/' # Classes ending with "Facade"
140+
- '/FacadeInterface$/' # Classes ending with "FacadeInterface"
141+
- '/Input$/' # Classes ending with "Input"
142+
- '/Result$/' # Classes ending with "Result"
136143
tags:
137144
- phpstan.rules.rule
138145
```
@@ -183,10 +190,11 @@ src/Capability/
183190
- Format: `LayerName: [AllowedDependency1, AllowedDependency2, ...]`
184191
- Default layers: Domain, Application, Infrastructure, Presentation
185192
- You can define any custom layer names you need
186-
- `allowedCrossModulePatterns`: (Optional) Regex patterns for class names that can be imported across modules.
187-
- Default patterns: `/Facade$/`, `/FacadeInterface$/`, `/Input$/`, `/Result$/`
193+
- `allowedCrossModulePatterns`: **Required** - Regex patterns for class names that can be imported across modules.
194+
- **No defaults** - you must explicitly configure which classes can cross module boundaries
195+
- Common patterns: `/Facade$/`, `/FacadeInterface$/`, `/Input$/`, `/Result$/`
188196
- Each pattern is a regex that matches against the class name (not the full namespace)
189-
- You can add custom patterns to allow additional cross-module imports
197+
- Empty array `[]` = no cross-module imports allowed (complete module isolation)
190198

191199
**Example Violations:**
192200

src/Architecture/ModularArchitectureRule.php

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,16 @@ class ModularArchitectureRule implements Rule
5555
* @param array<string, array<string>>|null $layerDependencies Custom layer dependency rules.
5656
* Format: ['LayerName' => ['AllowedLayer1', 'AllowedLayer2']]
5757
* If null, uses default hexagonal architecture rules.
58-
* @param array<string>|null $allowedCrossModulePatterns Regex patterns for class names that can be imported cross-module.
59-
* If null, uses default patterns (Facade, FacadeInterface, Input, Result).
58+
* @param array<string> $allowedCrossModulePatterns Regex patterns for class names that can be imported cross-module.
59+
* Example: ['/Facade$/', '/FacadeInterface$/', '/Input$/', '/Result$/']
6060
*/
6161
public function __construct(
6262
private string $baseNamespace,
6363
?array $layerDependencies = null,
64-
?array $allowedCrossModulePatterns = null
64+
array $allowedCrossModulePatterns = []
6565
) {
6666
$this->layerDependencies = $layerDependencies ?? $this->getDefaultLayerDependencies();
67-
$this->allowedCrossModulePatterns = $allowedCrossModulePatterns ?? $this->getDefaultCrossModulePatterns();
67+
$this->allowedCrossModulePatterns = $allowedCrossModulePatterns;
6868
}
6969

7070
/**
@@ -82,21 +82,6 @@ private function getDefaultLayerDependencies(): array
8282
];
8383
}
8484

85-
/**
86-
* Get default patterns for allowed cross-module imports
87-
*
88-
* @return array<string>
89-
*/
90-
private function getDefaultCrossModulePatterns(): array
91-
{
92-
return [
93-
'/Facade$/', // Classes ending with "Facade"
94-
'/FacadeInterface$/', // Classes ending with "FacadeInterface"
95-
'/Input$/', // Classes ending with "Input"
96-
'/Result$/', // Classes ending with "Result"
97-
];
98-
}
99-
10085
public function getNodeType(): string
10186
{
10287
return Use_::class;
@@ -316,10 +301,10 @@ private function validateCrossModuleDependency(
316301

317302
if (!$isAllowed) {
318303
return RuleErrorBuilder::message(sprintf(
319-
'Cross-module violation: Module `%s` can only import facades, Input, or Result classes from module `%s`. Cannot import `%s`.',
304+
'Cross-module violation: Module `%s` is not allowed to import `%s` from module `%s`.',
320305
$sourceModuleInfo['module'],
321-
$targetModuleInfo['module'],
322-
$usedClassName
306+
$usedClassName,
307+
$targetModuleInfo['module']
323308
))
324309
->identifier(self::IDENTIFIER_CROSS_MODULE)
325310
->line($line)

tests/TestCases/Architecture/ModularArchitectureCustomCrossModuleRuleTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public function testStillBlocksNonMatchingCrossModuleImports(): void
4848
[__DIR__ . '/../../../data/ModularArchitectureTest/Capability/ProductCatalog/Application/InvalidCrossModule.php'],
4949
[
5050
[
51-
'Cross-module violation: Module `ProductCatalog` can only import facades, Input, or Result classes from module `UserManagement`. Cannot import `App\Capability\UserManagement\UserManagementException`.',
51+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\UserManagementException` from module `UserManagement`.',
5252
7,
5353
],
5454
]

tests/TestCases/Architecture/ModularArchitectureCustomLayersRuleTest.php

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,21 @@ class ModularArchitectureCustomLayersRuleTest extends RuleTestCase
1818
protected function getRule(): Rule
1919
{
2020
// Custom configuration: Allow Application to depend on Infrastructure
21-
return new ModularArchitectureRule('App\\Capability', [
22-
'Domain' => [],
23-
'Application' => ['Domain', 'Infrastructure'], // Custom: Application CAN depend on Infrastructure
24-
'Infrastructure' => ['Domain'],
25-
'Presentation' => ['Application', 'Domain'],
26-
]);
21+
return new ModularArchitectureRule(
22+
'App\\Capability',
23+
[
24+
'Domain' => [],
25+
'Application' => ['Domain', 'Infrastructure'], // Custom: Application CAN depend on Infrastructure
26+
'Infrastructure' => ['Domain'],
27+
'Presentation' => ['Application', 'Domain'],
28+
],
29+
[
30+
'/Facade$/',
31+
'/FacadeInterface$/',
32+
'/Input$/',
33+
'/Result$/',
34+
]
35+
);
2736
}
2837

2938

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Phauthentic\PHPStanRules\Tests\TestCases\Architecture;
6+
7+
use Phauthentic\PHPStanRules\Architecture\ModularArchitectureRule;
8+
use PHPStan\Rules\Rule;
9+
use PHPStan\Testing\RuleTestCase;
10+
11+
/**
12+
* Test that without cross-module patterns, all cross-module imports are blocked
13+
*
14+
* @extends RuleTestCase<ModularArchitectureRule>
15+
*/
16+
class ModularArchitectureNoCrossModuleRuleTest extends RuleTestCase
17+
{
18+
protected function getRule(): Rule
19+
{
20+
// No cross-module patterns - blocks all cross-module imports
21+
return new ModularArchitectureRule('App\\Capability', null, []);
22+
}
23+
24+
public function testNoCrossModulePatternsBlocksAllImports(): void
25+
{
26+
// Without any cross-module patterns, even Facade/Input/Result should be blocked
27+
$this->analyse(
28+
[__DIR__ . '/../../../data/ModularArchitectureTest/Capability/ProductCatalog/Application/ValidCrossModule.php'],
29+
[
30+
[
31+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\UserManagementFacade` from module `UserManagement`.',
32+
7,
33+
],
34+
[
35+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\UserManagementFacadeInterface` from module `UserManagement`.',
36+
8,
37+
],
38+
[
39+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\Application\UseCases\CreateUser\CreateUserInput` from module `UserManagement`.',
40+
9,
41+
],
42+
[
43+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\Application\UseCases\CreateUser\CreateUserResult` from module `UserManagement`.',
44+
10,
45+
],
46+
]
47+
);
48+
}
49+
50+
public function testIntraModuleDependenciesStillWork(): void
51+
{
52+
// Intra-module dependencies should still work (layer rules)
53+
$this->analyse(
54+
[__DIR__ . '/../../../data/ModularArchitectureTest/Capability/UserManagement/Application/UseCases/CreateUser/CreateUser.php'],
55+
[]
56+
);
57+
}
58+
}
59+

tests/TestCases/Architecture/ModularArchitectureRuleTest.php

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,16 @@ class ModularArchitectureRuleTest extends RuleTestCase
1515
{
1616
protected function getRule(): Rule
1717
{
18-
return new ModularArchitectureRule('App\\Capability');
18+
return new ModularArchitectureRule(
19+
'App\\Capability',
20+
null,
21+
[
22+
'/Facade$/',
23+
'/FacadeInterface$/',
24+
'/Input$/',
25+
'/Result$/',
26+
]
27+
);
1928
}
2029

2130

@@ -127,7 +136,7 @@ public function testInvalidCrossModuleExceptionImport(): void
127136
[__DIR__ . '/../../../data/ModularArchitectureTest/Capability/ProductCatalog/Application/InvalidCrossModule.php'],
128137
[
129138
[
130-
'Cross-module violation: Module `ProductCatalog` can only import facades, Input, or Result classes from module `UserManagement`. Cannot import `App\Capability\UserManagement\UserManagementException`.',
139+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\UserManagementException` from module `UserManagement`.',
131140
7,
132141
],
133142
]
@@ -165,7 +174,7 @@ public function testCustomDtoNotAllowedByDefault(): void
165174
[__DIR__ . '/../../../data/ModularArchitectureTest/Capability/ProductCatalog/Application/UseCustomDto.php'],
166175
[
167176
[
168-
'Cross-module violation: Module `ProductCatalog` can only import facades, Input, or Result classes from module `UserManagement`. Cannot import `App\Capability\UserManagement\UserManagementDto`.',
177+
'Cross-module violation: Module `ProductCatalog` is not allowed to import `App\Capability\UserManagement\UserManagementDto` from module `UserManagement`.',
169178
7,
170179
],
171180
]

0 commit comments

Comments
 (0)