Skip to content

Commit 2f39bc5

Browse files
committed
fixup!
1 parent 972b305 commit 2f39bc5

File tree

4 files changed

+181
-67
lines changed

4 files changed

+181
-67
lines changed

src/RuleBuilders/Architecture/Architecture.php

Lines changed: 40 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,29 @@
33

44
namespace Arkitect\RuleBuilders\Architecture;
55

6-
use Arkitect\Expression\Boolean\Andx;
7-
use Arkitect\Expression\Boolean\Not;
6+
use Arkitect\Expression\Boolean\Orx;
87
use Arkitect\Expression\Expression;
9-
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
10-
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
8+
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseExpressions;
119
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
1210
use Arkitect\Rules\Rule;
1311

1412
class Architecture implements Component, DefinedBy, Where, MayDependOnComponents, MayDependOnAnyComponent, ShouldNotDependOnAnyComponent, ShouldOnlyDependOnComponents, Rules
1513
{
1614
/** @var string */
1715
private $componentName;
18-
/** @var array<string, string> */
16+
/** @var array<string, Expression|string> */
1917
private $componentSelectors;
2018
/** @var array<string, string[]> */
2119
private $allowedDependencies;
2220
/** @var array<string, string[]> */
23-
private $componentDependsOnlyOnTheseNamespaces;
21+
private $componentDependsOnlyOnTheseComponents;
2422

2523
private function __construct()
2624
{
2725
$this->componentName = '';
2826
$this->componentSelectors = [];
2927
$this->allowedDependencies = [];
30-
$this->componentDependsOnlyOnTheseNamespaces = [];
28+
$this->componentDependsOnlyOnTheseComponents = [];
3129
}
3230

3331
public static function withComponents(): Component
@@ -72,7 +70,7 @@ public function shouldNotDependOnAnyComponent()
7270

7371
public function shouldOnlyDependOnComponents(string ...$componentNames)
7472
{
75-
$this->componentDependsOnlyOnTheseNamespaces[$this->componentName] = $componentNames;
73+
$this->componentDependsOnlyOnTheseComponents[$this->componentName] = $componentNames;
7674

7775
return $this;
7876
}
@@ -93,63 +91,61 @@ public function mayDependOnAnyComponent()
9391

9492
public function rules(): iterable
9593
{
96-
$layerNames = array_keys($this->componentSelectors);
97-
9894
foreach ($this->componentSelectors as $name => $selector) {
9995
if (isset($this->allowedDependencies[$name])) {
100-
$forbiddenComponents = array_diff($layerNames, [$name], $this->allowedDependencies[$name]);
101-
102-
if (!empty($forbiddenComponents)) {
103-
yield Rule::allClasses()
104-
->that(\is_string($selector) ? new ResideInOneOfTheseNamespaces($selector) : $selector)
105-
->should($this->createForbiddenExpression($forbiddenComponents))
106-
->because('of component architecture');
107-
}
96+
yield Rule::allClasses()
97+
->that(\is_string($selector) ? new ResideInOneOfTheseNamespaces($selector) : $selector)
98+
->should($this->createAllowedExpression(
99+
array_merge([$name], $this->allowedDependencies[$name])
100+
))
101+
->because('of component architecture');
108102
}
109103

110-
if (!isset($this->componentDependsOnlyOnTheseNamespaces[$name])) {
111-
continue;
104+
if (isset($this->componentDependsOnlyOnTheseComponents[$name])) {
105+
yield Rule::allClasses()
106+
->that(\is_string($selector) ? new ResideInOneOfTheseNamespaces($selector) : $selector)
107+
->should($this->createAllowedExpression($this->componentDependsOnlyOnTheseComponents[$name]))
108+
->because('of component architecture');
112109
}
110+
}
111+
}
113112

114-
$allowedDependencies = array_map(function (string $componentName): string {
115-
return $this->componentSelectors[$componentName];
116-
}, $this->componentDependsOnlyOnTheseNamespaces[$name]);
113+
private function createAllowedExpression(array $components): Expression
114+
{
115+
$namespaceSelectors = $this->extractComponentsNamespaceSelectors($components);
116+
117+
$expressionSelectors = $this->extractComponentExpressionSelectors($components);
118+
119+
if ([] === $namespaceSelectors && [] === $expressionSelectors) {
120+
return new Orx(); // always true
121+
}
117122

118-
yield Rule::allClasses()
119-
->that(new ResideInOneOfTheseNamespaces($selector))
120-
->should(new DependsOnlyOnTheseNamespaces(...$allowedDependencies))
121-
->because('of component architecture');
123+
if ([] !== $namespaceSelectors) {
124+
$expressionSelectors[] = new ResideInOneOfTheseNamespaces(...$namespaceSelectors);
122125
}
126+
127+
return new DependsOnlyOnTheseExpressions(...$expressionSelectors);
123128
}
124129

125-
public function createForbiddenExpression(array $forbiddenComponents): Expression
130+
private function extractComponentsNamespaceSelectors(array $components): array
126131
{
127-
$forbiddenNamespaceSelectors = array_filter(
132+
return array_filter(
128133
array_map(function (string $componentName): ?string {
129134
$selector = $this->componentSelectors[$componentName];
130135

131136
return \is_string($selector) ? $selector : null;
132-
}, $forbiddenComponents)
137+
}, $components)
133138
);
139+
}
134140

135-
$forbiddenExpressionSelectors = array_filter(
141+
private function extractComponentExpressionSelectors(array $components): array
142+
{
143+
return array_filter(
136144
array_map(function (string $componentName): ?Expression {
137145
$selector = $this->componentSelectors[$componentName];
138146

139147
return \is_string($selector) ? null : $selector;
140-
}, $forbiddenComponents)
148+
}, $components)
141149
);
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);
154150
}
155151
}

tests/Fixtures/Fruit/AnimalFruit.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Arkitect\Tests\Fixtures\Fruit;
6+
7+
use Arkitect\Tests\Fixtures\Animal\Cat;
8+
9+
final class AnimalFruit extends Banana
10+
{
11+
/**
12+
* @var Cat
13+
*/
14+
private $cat;
15+
16+
public function __construct(Cat $cat)
17+
{
18+
$this->cat = $cat;
19+
}
20+
}

tests/Unit/Architecture/ArchitectureTest.php

Lines changed: 43 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,7 @@
33

44
namespace Arkitect\Tests\Unit\Architecture;
55

6-
use Arkitect\Expression\Boolean\Andx;
7-
use Arkitect\Expression\Boolean\Not;
8-
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
9-
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
6+
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseExpressions;
107
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
118
use Arkitect\RuleBuilders\Architecture\Architecture;
129
use Arkitect\Rules\Rule;
@@ -30,15 +27,26 @@ public function test_layered_architecture(): void
3027
$expectedRules = [
3128
Rule::allClasses()
3229
->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
33-
->should(new NotDependsOnTheseNamespaces('App\*\Application\*', 'App\*\Infrastructure\*'))
30+
->should(new DependsOnlyOnTheseExpressions(
31+
new ResideInOneOfTheseNamespaces('App\*\Domain\*')
32+
))
3433
->because('of component architecture'),
3534
Rule::allClasses()
3635
->that(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
37-
->should(new NotDependsOnTheseNamespaces('App\*\Infrastructure\*'))
36+
->should(new DependsOnlyOnTheseExpressions(
37+
new ResideInOneOfTheseNamespaces('App\*\Application\*', 'App\*\Domain\*')
38+
))
39+
->because('of component architecture'),
40+
Rule::allClasses()
41+
->that(new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*'))
42+
->should(new DependsOnlyOnTheseExpressions(
43+
new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*', 'App\*\Domain\*', 'App\*\Application\*')
44+
))
3845
->because('of component architecture'),
3946
];
4047

41-
self::assertEquals($expectedRules, iterator_to_array($rules));
48+
$actualRules = iterator_to_array($rules);
49+
self::assertEquals($expectedRules, $actualRules);
4250
}
4351

4452
public function test_layered_architecture_with_expression(): void
@@ -58,20 +66,26 @@ public function test_layered_architecture_with_expression(): void
5866
$expectedRules = [
5967
Rule::allClasses()
6068
->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
61-
->should(new Not(new Andx(
62-
new ResideInOneOfTheseNamespaces('App\*\Application\*'),
63-
new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*')
64-
)))
69+
->should(new DependsOnlyOnTheseExpressions(
70+
new ResideInOneOfTheseNamespaces('App\*\Domain\*')
71+
))
6572
->because('of component architecture'),
6673
Rule::allClasses()
6774
->that(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
68-
->should(new Not(new Andx(
69-
new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*')
70-
)))
75+
->should(new DependsOnlyOnTheseExpressions(
76+
new ResideInOneOfTheseNamespaces('App\*\Application\*', 'App\*\Domain\*')
77+
))
78+
->because('of component architecture'),
79+
Rule::allClasses()
80+
->that(new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*'))
81+
->should(new DependsOnlyOnTheseExpressions(
82+
new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*', 'App\*\Domain\*', 'App\*\Application\*')
83+
))
7184
->because('of component architecture'),
7285
];
7386

74-
self::assertEquals($expectedRules, iterator_to_array($rules));
87+
$actualRules = iterator_to_array($rules);
88+
self::assertEquals($expectedRules, $actualRules);
7589
}
7690

7791
public function test_layered_architecture_with_mix_of_namespace_and_expression(): void
@@ -90,20 +104,26 @@ public function test_layered_architecture_with_mix_of_namespace_and_expression()
90104
$expectedRules = [
91105
Rule::allClasses()
92106
->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-
))
107+
->should(new DependsOnlyOnTheseExpressions(
108+
new ResideInOneOfTheseNamespaces('App\*\Domain\*')
98109
))
99110
->because('of component architecture'),
100111
Rule::allClasses()
101112
->that(new ResideInOneOfTheseNamespaces('App\*\Application\*'))
102-
->should(new NotDependsOnTheseNamespaces('App\*\Infrastructure\*'))
113+
->should(new DependsOnlyOnTheseExpressions(
114+
new ResideInOneOfTheseNamespaces('App\*\Application\*', 'App\*\Domain\*')
115+
))
116+
->because('of component architecture'),
117+
Rule::allClasses()
118+
->that(new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*'))
119+
->should(new DependsOnlyOnTheseExpressions(
120+
new ResideInOneOfTheseNamespaces('App\*\Domain\*', 'App\*\Application\*', 'App\*\Infrastructure\*')
121+
))
103122
->because('of component architecture'),
104123
];
105124

106-
self::assertEquals($expectedRules, iterator_to_array($rules));
125+
$actualRules = iterator_to_array($rules);
126+
self::assertEquals($expectedRules, $actualRules);
107127
}
108128

109129
public function test_layered_architecture_with_depends_only_on_components(): void
@@ -117,7 +137,7 @@ public function test_layered_architecture_with_depends_only_on_components(): voi
117137
$expectedRules = [
118138
Rule::allClasses()
119139
->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
120-
->should(new DependsOnlyOnTheseNamespaces('App\*\Domain\*'))
140+
->should(new DependsOnlyOnTheseExpressions(new ResideInOneOfTheseNamespaces('App\*\Domain\*')))
121141
->because('of component architecture'),
122142
];
123143

tests/Unit/Rules/RuleCheckerTest.php

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@
44
namespace Arkitect\Tests\Unit\Rules;
55

66
use Arkitect\Analyzer\ClassDescription;
7+
use Arkitect\Analyzer\FileParserFactory;
78
use Arkitect\Analyzer\Parser;
89
use Arkitect\ClassSet;
910
use Arkitect\ClassSetRules;
1011
use Arkitect\CLI\Progress\VoidProgress;
1112
use Arkitect\CLI\Runner;
13+
use Arkitect\CLI\TargetPhpVersion;
14+
use Arkitect\Expression\ForClasses\HaveNameMatching;
15+
use Arkitect\Expression\ForClasses\Implement;
16+
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
1217
use Arkitect\Rules\DSL\ArchRule;
1318
use Arkitect\Rules\ParsingErrors;
19+
use Arkitect\Rules\Rule;
1420
use Arkitect\Rules\Violation;
1521
use Arkitect\Rules\Violations;
22+
use Arkitect\Tests\Fixtures\Animal\AnimalInterface;
23+
use Arkitect\Tests\Fixtures\Fruit\AnimalFruit;
24+
use Arkitect\Tests\Fixtures\Fruit\CavendishBanana;
25+
use Arkitect\Tests\Fixtures\Fruit\DwarfCavendishBanana;
26+
use Arkitect\Tests\Fixtures\Fruit\FruitInterface;
1627
use PHPUnit\Framework\TestCase;
1728
use Symfony\Component\Finder\SplFileInfo;
1829

@@ -37,6 +48,73 @@ public function test_should_run_parse_on_all_files_in_class_set(): void
3748

3849
self::assertCount(3, $violations);
3950
}
51+
52+
public function test_can_exclude_files_or_directories_from_multiple_dir_class_set_with_no_violations(): void
53+
{
54+
$classSet = ClassSet::fromDir(\FIXTURES_PATH);
55+
56+
$rules[] = Rule::allClasses()
57+
->except(FruitInterface::class, CavendishBanana::class, DwarfCavendishBanana::class, AnimalFruit::class)
58+
->that(new ResideInOneOfTheseNamespaces('Arkitect\Tests\Fixtures\Fruit'))
59+
->should(new Implement(FruitInterface::class))
60+
->because('this tests that string exceptions fail');
61+
62+
$rules[] = Rule::allClasses()
63+
->exceptExpression(new HaveNameMatching('*TestCase'))
64+
->that(new ResideInOneOfTheseNamespaces('Arkitect\Tests\Fixtures\Animal'))
65+
->should(new Implement(AnimalInterface::class))
66+
->because('this tests that expression exceptions fail');
67+
68+
$runner = new Runner();
69+
70+
$runner->check(
71+
ClassSetRules::create($classSet, ...$rules),
72+
new VoidProgress(),
73+
FileParserFactory::createFileParser(TargetPhpVersion::create(null)),
74+
$violations = new Violations(),
75+
new ParsingErrors()
76+
);
77+
78+
self::assertCount(0, $violations);
79+
}
80+
81+
public function test_can_exclude_files_or_directories_from_multiple_dir_class_set_with_violations(): void
82+
{
83+
$classSet = ClassSet::fromDir(\FIXTURES_PATH);
84+
85+
$rules[] = Rule::allClasses()
86+
->except(FruitInterface::class, CavendishBanana::class, AnimalFruit::class)
87+
->that(new ResideInOneOfTheseNamespaces('Arkitect\Tests\Fixtures\Fruit'))
88+
->should(new Implement(FruitInterface::class))
89+
->because('this tests that string exceptions fail');
90+
91+
$rules[] = Rule::allClasses()
92+
->exceptExpression(new HaveNameMatching('*NotExistingSoItFails'))
93+
->that(new ResideInOneOfTheseNamespaces('Arkitect\Tests\Fixtures\Animal'))
94+
->should(new Implement(AnimalInterface::class))
95+
->because('this tests that expression exceptions fail');
96+
97+
$runner = new Runner();
98+
99+
$runner->check(
100+
ClassSetRules::create($classSet, ...$rules),
101+
new VoidProgress(),
102+
FileParserFactory::createFileParser(TargetPhpVersion::create(null)),
103+
$violations = new Violations(),
104+
new ParsingErrors()
105+
);
106+
107+
self::assertCount(2, $violations);
108+
$expectedViolations = "Arkitect\Tests\Fixtures\Animal\CatTestCase has 1 violations
109+
should implement Arkitect\Tests\Fixtures\Animal\AnimalInterface because this tests
110+
that expression exceptions fail Arkitect\Tests\Fixtures\Fruit\DwarfCavendishBanana has 1 violations
111+
should implement Arkitect\Tests\Fixtures\Fruit\FruitInterface because
112+
this tests that string exceptions fail";
113+
self::assertEquals(
114+
preg_replace('/\s+/', ' ', $expectedViolations),
115+
preg_replace('/\s+/', ' ', trim($violations->toString()))
116+
);
117+
}
40118
}
41119

42120
class FakeClassSet extends ClassSet

0 commit comments

Comments
 (0)