Skip to content

Commit 134765d

Browse files
Herberto Gracahgraca
authored andcommitted
Allow architectures to be created from expressions
This will allow for composable and more complex rules.
1 parent 5d965f4 commit 134765d

File tree

3 files changed

+115
-6
lines changed

3 files changed

+115
-6
lines changed

src/RuleBuilders/Architecture/Architecture.php

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33

44
namespace Arkitect\RuleBuilders\Architecture;
55

6+
use Arkitect\Expression\Boolean\Andx;
7+
use Arkitect\Expression\Boolean\Not;
8+
use Arkitect\Expression\Expression;
69
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
710
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
811
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
@@ -46,6 +49,13 @@ public function definedBy(string $selector)
4649
return $this;
4750
}
4851

52+
public function definedByExpression(Expression $selector)
53+
{
54+
$this->componentSelectors[$this->componentName] = $selector;
55+
56+
return $this;
57+
}
58+
4959
public function where(string $componentName)
5060
{
5161
$this->componentName = $componentName;
@@ -90,13 +100,9 @@ public function rules(): iterable
90100
$forbiddenComponents = array_diff($layerNames, [$name], $this->allowedDependencies[$name]);
91101

92102
if (!empty($forbiddenComponents)) {
93-
$forbiddenSelectors = array_map(function (string $componentName): string {
94-
return $this->componentSelectors[$componentName];
95-
}, $forbiddenComponents);
96-
97103
yield Rule::allClasses()
98-
->that(new ResideInOneOfTheseNamespaces($selector))
99-
->should(new NotDependsOnTheseNamespaces(...$forbiddenSelectors))
104+
->that(\is_string($selector) ? new ResideInOneOfTheseNamespaces($selector) : $selector)
105+
->should($this->createForbiddenExpression($forbiddenComponents))
100106
->because('of component architecture');
101107
}
102108
}
@@ -115,4 +121,35 @@ public function rules(): iterable
115121
->because('of component architecture');
116122
}
117123
}
124+
125+
public function createForbiddenExpression(array $forbiddenComponents): Expression
126+
{
127+
$forbiddenNamespaceSelectors = array_filter(
128+
array_map(function (string $componentName): ?string {
129+
$selector = $this->componentSelectors[$componentName];
130+
131+
return \is_string($selector) ? $selector : null;
132+
}, $forbiddenComponents)
133+
);
134+
135+
$forbiddenExpressionSelectors = array_filter(
136+
array_map(function (string $componentName): ?Expression {
137+
$selector = $this->componentSelectors[$componentName];
138+
139+
return \is_string($selector) ? null : $selector;
140+
}, $forbiddenComponents)
141+
);
142+
143+
$forbiddenExpressionList = [];
144+
if ([] !== $forbiddenNamespaceSelectors) {
145+
$forbiddenExpressionList[] = new NotDependsOnTheseNamespaces(...$forbiddenNamespaceSelectors);
146+
}
147+
if ([] !== $forbiddenExpressionSelectors) {
148+
$forbiddenExpressionList[] = new Not(new Andx(...$forbiddenExpressionSelectors));
149+
}
150+
151+
return 1 === \count($forbiddenExpressionList)
152+
? array_pop($forbiddenExpressionList)
153+
: new Andx(...$forbiddenExpressionList);
154+
}
118155
}

src/RuleBuilders/Architecture/DefinedBy.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,13 @@
33

44
namespace Arkitect\RuleBuilders\Architecture;
55

6+
use Arkitect\Expression\Expression;
7+
68
interface DefinedBy
79
{
810
/** @return Component&Where */
911
public function definedBy(string $selector);
12+
13+
/** @return Component&Where */
14+
public function definedByExpression(Expression $selector);
1015
}

tests/Unit/Architecture/ArchitectureTest.php

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
namespace Arkitect\Tests\Unit\Architecture;
55

6+
use Arkitect\Expression\Boolean\Andx;
7+
use Arkitect\Expression\Boolean\Not;
68
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
79
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
810
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
@@ -39,6 +41,71 @@ public function test_layered_architecture(): void
3941
self::assertEquals($expectedRules, iterator_to_array($rules));
4042
}
4143

44+
public function test_layered_architecture_with_expression(): void
45+
{
46+
$rules = Architecture::withComponents()
47+
->component('Domain')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
48+
->component('Application')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
49+
->component('Infrastructure')
50+
->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*'))
51+
52+
->where('Domain')->shouldNotDependOnAnyComponent()
53+
->where('Application')->mayDependOnComponents('Domain')
54+
->where('Infrastructure')->mayDependOnAnyComponent()
55+
56+
->rules();
57+
58+
$expectedRules = [
59+
Rule::allClasses()
60+
->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
61+
->should(new Not(new Andx(
62+
new ResideInOneOfTheseNamespaces('App\*\Application\*'),
63+
new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*')
64+
)))
65+
->because('of component architecture'),
66+
Rule::allClasses()
67+
->that(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
68+
->should(new Not(new Andx(
69+
new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*')
70+
)))
71+
->because('of component architecture'),
72+
];
73+
74+
self::assertEquals($expectedRules, iterator_to_array($rules));
75+
}
76+
77+
public function test_layered_architecture_with_mix_of_namespace_and_expression(): void
78+
{
79+
$rules = Architecture::withComponents()
80+
->component('Domain')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
81+
->component('Application')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
82+
->component('Infrastructure')->definedBy('App\*\Infrastructure\*')
83+
84+
->where('Domain')->shouldNotDependOnAnyComponent()
85+
->where('Application')->mayDependOnComponents('Domain')
86+
->where('Infrastructure')->mayDependOnAnyComponent()
87+
88+
->rules();
89+
90+
$expectedRules = [
91+
Rule::allClasses()
92+
->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
93+
->should(new Andx(
94+
new NotDependsOnTheseNamespaces('App\*\Infrastructure\*'),
95+
new Not(new Andx(
96+
new ResideInOneOfTheseNamespaces('App\*\Application\*')
97+
))
98+
))
99+
->because('of component architecture'),
100+
Rule::allClasses()
101+
->that(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
102+
->should(new NotDependsOnTheseNamespaces('App\*\Infrastructure\*'))
103+
->because('of component architecture'),
104+
];
105+
106+
self::assertEquals($expectedRules, iterator_to_array($rules));
107+
}
108+
42109
public function test_layered_architecture_with_depends_only_on_components(): void
43110
{
44111
$rules = Architecture::withComponents()

0 commit comments

Comments
 (0)