Skip to content

Commit b4ef086

Browse files
author
Herberto Graca
committed
Allow architectures to be created from expressions
This will allow for composable and more complex rules.
1 parent a43a6f6 commit b4ef086

File tree

3 files changed

+113
-6
lines changed

3 files changed

+113
-6
lines changed

src/RuleBuilders/Architecture/Architecture.php

+41-6
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33

44
namespace Arkitect\RuleBuilders\Architecture;
55

6+
use Arkitect\Expression\Expression;
7+
use Arkitect\Expression\ForClasses\Andx;
68
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
9+
use Arkitect\Expression\ForClasses\Not;
710
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
811
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
912
use Arkitect\Rules\Rule;
@@ -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,33 @@ 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+
return \is_string($selector) ? $selector : null;
131+
}, $forbiddenComponents)
132+
);
133+
134+
$forbiddenExpressionSelectors = array_filter(
135+
array_map(function (string $componentName): ?Expression {
136+
$selector = $this->componentSelectors[$componentName];
137+
return \is_string($selector) ? null : $selector;
138+
}, $forbiddenComponents)
139+
);
140+
141+
$forbiddenExpressionList = [];
142+
if ($forbiddenNamespaceSelectors !== []) {
143+
$forbiddenExpressionList[] = new NotDependsOnTheseNamespaces(...$forbiddenNamespaceSelectors);
144+
}
145+
if ($forbiddenExpressionSelectors !== []) {
146+
$forbiddenExpressionList[] = new Not(new Andx(...$forbiddenExpressionSelectors));
147+
}
148+
149+
return count($forbiddenExpressionList) === 1
150+
? array_pop($forbiddenExpressionList)
151+
: new Andx(...$forbiddenExpressionList);
152+
}
118153
}

src/RuleBuilders/Architecture/DefinedBy.php

+5
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

+67
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33

44
namespace Arkitect\Tests\Unit\Architecture;
55

6+
use Arkitect\Expression\ForClasses\Andx;
67
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
8+
use Arkitect\Expression\ForClasses\Not;
79
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
810
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
911
use Arkitect\RuleBuilders\Architecture\Architecture;
@@ -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)