Skip to content

Commit b057470

Browse files
committed
Create DependsOnlyOnTheseExpressions
1 parent e5e7140 commit b057470

File tree

2 files changed

+217
-0
lines changed

2 files changed

+217
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Arkitect\Expression\ForClasses;
6+
7+
use Arkitect\Analyzer\ClassDependency;
8+
use Arkitect\Analyzer\ClassDescription;
9+
use Arkitect\Analyzer\FileParser;
10+
use Arkitect\Analyzer\FileParserFactory;
11+
use Arkitect\Expression\Description;
12+
use Arkitect\Expression\Expression;
13+
use Arkitect\Expression\MergeableExpression;
14+
use Arkitect\Rules\Violation;
15+
use Arkitect\Rules\ViolationMessage;
16+
use Arkitect\Rules\Violations;
17+
18+
class DependsOnlyOnTheseExpressions implements Expression
19+
{
20+
/** @var FileParser */
21+
private $fileParser;
22+
23+
/** @var Expression[] */
24+
private $expressions = [];
25+
26+
public function __construct(Expression ...$expressions)
27+
{
28+
$this->fileParser = FileParserFactory::createFileParser();
29+
foreach ($expressions as $newExpression) {
30+
$this->addExpression($newExpression);
31+
}
32+
}
33+
34+
public function describe(ClassDescription $theClass, string $because): Description
35+
{
36+
$expressionsDescriptions = '';
37+
foreach ($this->expressions as $expression) {
38+
$expressionsDescriptions .= $expression->describe($theClass, '')->toString()."\n";
39+
}
40+
41+
return new Description(
42+
"should depend only on classes in one of the given expressions: \n"
43+
.$expressionsDescriptions,
44+
$because
45+
);
46+
}
47+
48+
public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
49+
{
50+
$dependencies = $this->removeDuplicateDependencies($theClass->getDependencies());
51+
52+
foreach ($dependencies as $dependency) {
53+
if (
54+
'' === $dependency->getFQCN()->namespace()
55+
|| $theClass->namespaceMatches($dependency->getFQCN()->namespace())
56+
) {
57+
continue;
58+
}
59+
60+
$dependencyClassDescription = $this->getDependencyClassDescription($dependency);
61+
if (null === $dependencyClassDescription) {
62+
return;
63+
}
64+
65+
if (!$this->matchesAnyOfTheExpressions($dependencyClassDescription)) {
66+
$violations->add(
67+
Violation::create(
68+
$theClass->getFQCN(),
69+
ViolationMessage::withDescription(
70+
$this->describe($theClass, $because),
71+
"The dependency '".$dependencyClassDescription->getFQCN()."' violated the expression: \n"
72+
.$this->describeDependencyRequirement($dependencyClassDescription)."\n"
73+
)
74+
)
75+
);
76+
}
77+
}
78+
}
79+
80+
private function describeDependencyRequirement(ClassDescription $theDependency): string
81+
{
82+
$expressionsDescriptions = [];
83+
foreach ($this->expressions as $expression) {
84+
$expressionsDescriptions[] = $expression->describe($theDependency, '')->toString();
85+
}
86+
87+
return implode("\nOR\n", array_unique($expressionsDescriptions));
88+
}
89+
90+
private function matchesAnyOfTheExpressions(ClassDescription $dependencyClassDescription): bool
91+
{
92+
foreach ($this->expressions as $expression) {
93+
$newViolations = new Violations();
94+
$expression->evaluate($dependencyClassDescription, $newViolations, '');
95+
if (0 === $newViolations->count()) {
96+
return true;
97+
}
98+
}
99+
100+
return false;
101+
}
102+
103+
/**
104+
* @param ClassDependency $dependency
105+
*/
106+
private function getDependencyClassDescription($dependency): ?ClassDescription
107+
{
108+
/** @var class-string $dependencyFqcn */
109+
$dependencyFqcn = $dependency->getFQCN()->toString();
110+
$reflector = new \ReflectionClass($dependencyFqcn);
111+
$filename = $reflector->getFileName();
112+
if (false === $filename) {
113+
return null;
114+
}
115+
$this->fileParser->parse(file_get_contents($filename), $filename);
116+
$classDescriptionList = $this->fileParser->getClassDescriptions();
117+
118+
return array_pop($classDescriptionList);
119+
}
120+
121+
/**
122+
* @param ClassDependency[] $dependenciesList
123+
*
124+
* @return ClassDependency[]
125+
*/
126+
private function removeDuplicateDependencies(array $dependenciesList): array
127+
{
128+
$filteredList = [];
129+
foreach ($dependenciesList as $classDependency) {
130+
$dependencyFqcn = $classDependency->getFQCN()->toString();
131+
if (\array_key_exists($dependencyFqcn, $filteredList)) {
132+
continue;
133+
}
134+
$filteredList[$dependencyFqcn] = $classDependency;
135+
}
136+
137+
return $filteredList;
138+
}
139+
140+
private function addExpression(Expression $newExpression): void
141+
{
142+
foreach ($this->expressions as $index => $existingExpression) {
143+
if (
144+
$newExpression instanceof $existingExpression
145+
&& $newExpression instanceof MergeableExpression
146+
&& $existingExpression instanceof MergeableExpression
147+
) {
148+
$this->expressions[$index] = $existingExpression->mergeWith($newExpression);
149+
150+
return;
151+
}
152+
}
153+
$this->expressions[] = $newExpression;
154+
}
155+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Arkitect\Tests\Unit\Expressions\ForClasses;
6+
7+
use Arkitect\Analyzer\ClassDescription;
8+
use Arkitect\Analyzer\FileParserFactory;
9+
use Arkitect\Expression\ForClasses\DependsOnlyOnTheseExpressions;
10+
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
11+
use Arkitect\Rules\Violations;
12+
use PHPUnit\Framework\TestCase;
13+
14+
class DependsOnlyOnTheseExpressionsTest extends TestCase
15+
{
16+
public function test_it_should_have_no_violations_if_it_has_no_dependencies(): void
17+
{
18+
$dependsOnlyOnTheseExpressions = new DependsOnlyOnTheseExpressions(new ResideInOneOfTheseNamespaces('myNamespace'));
19+
20+
$classDescription = ClassDescription::getBuilder('HappyIsland\Myclass')->build();
21+
$because = 'we want to add this rule for our software';
22+
$violations = new Violations();
23+
$dependsOnlyOnTheseExpressions->evaluate($classDescription, $violations, $because);
24+
25+
self::assertEquals(0, $violations->count());
26+
}
27+
28+
public function test_it_should_have_no_violations_if_it_has_no_dependencies_outside_expression(): void
29+
{
30+
$dependsOnlyOnTheseExpressions = new DependsOnlyOnTheseExpressions(new ResideInOneOfTheseNamespaces('Arkitect\Tests\Fixtures\Fruit'));
31+
32+
$classDescription = $this->getClassDescription(file_get_contents(\FIXTURES_PATH.'/Fruit/Banana.php'));
33+
34+
$because = 'we want to add this rule for our software';
35+
$violations = new Violations();
36+
$dependsOnlyOnTheseExpressions->evaluate($classDescription, $violations, $because);
37+
38+
self::assertEquals(0, $violations->count());
39+
}
40+
41+
public function test_it_should_have_violations_if_it_has_dependencies_outside_expression(): void
42+
{
43+
$dependsOnlyOnTheseExpressions = new DependsOnlyOnTheseExpressions(new ResideInOneOfTheseNamespaces('Arkitect\Tests\Fixtures\Fruit'));
44+
45+
$classDescription = $this->getClassDescription(file_get_contents(\FIXTURES_PATH.'/Fruit/AnimalFruit.php'));
46+
47+
$because = 'we want to add this rule for our software';
48+
$violations = new Violations();
49+
$dependsOnlyOnTheseExpressions->evaluate($classDescription, $violations, $because);
50+
51+
self::assertEquals(1, $violations->count());
52+
}
53+
54+
private function getClassDescription(string $classCode, string $fileName = 'MyClass.php'): ClassDescription
55+
{
56+
$fileParser = FileParserFactory::createFileParser();
57+
$fileParser->parse($classCode, $fileName);
58+
$classDescriptionList = $fileParser->getClassDescriptions();
59+
60+
return array_pop($classDescriptionList);
61+
}
62+
}

0 commit comments

Comments
 (0)