From 134765d996d631057f5a3b37966b964c83558e2f Mon Sep 17 00:00:00 2001 From: Herberto Graca Date: Thu, 27 Jul 2023 14:50:13 +0200 Subject: [PATCH] Allow architectures to be created from expressions This will allow for composable and more complex rules. --- .../Architecture/Architecture.php | 49 ++++++++++++-- src/RuleBuilders/Architecture/DefinedBy.php | 5 ++ tests/Unit/Architecture/ArchitectureTest.php | 67 +++++++++++++++++++ 3 files changed, 115 insertions(+), 6 deletions(-) diff --git a/src/RuleBuilders/Architecture/Architecture.php b/src/RuleBuilders/Architecture/Architecture.php index a1b38832..ab23c935 100644 --- a/src/RuleBuilders/Architecture/Architecture.php +++ b/src/RuleBuilders/Architecture/Architecture.php @@ -3,6 +3,9 @@ namespace Arkitect\RuleBuilders\Architecture; +use Arkitect\Expression\Boolean\Andx; +use Arkitect\Expression\Boolean\Not; +use Arkitect\Expression\Expression; use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces; use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces; use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces; @@ -46,6 +49,13 @@ public function definedBy(string $selector) return $this; } + public function definedByExpression(Expression $selector) + { + $this->componentSelectors[$this->componentName] = $selector; + + return $this; + } + public function where(string $componentName) { $this->componentName = $componentName; @@ -90,13 +100,9 @@ public function rules(): iterable $forbiddenComponents = array_diff($layerNames, [$name], $this->allowedDependencies[$name]); if (!empty($forbiddenComponents)) { - $forbiddenSelectors = array_map(function (string $componentName): string { - return $this->componentSelectors[$componentName]; - }, $forbiddenComponents); - yield Rule::allClasses() - ->that(new ResideInOneOfTheseNamespaces($selector)) - ->should(new NotDependsOnTheseNamespaces(...$forbiddenSelectors)) + ->that(\is_string($selector) ? new ResideInOneOfTheseNamespaces($selector) : $selector) + ->should($this->createForbiddenExpression($forbiddenComponents)) ->because('of component architecture'); } } @@ -115,4 +121,35 @@ public function rules(): iterable ->because('of component architecture'); } } + + public function createForbiddenExpression(array $forbiddenComponents): Expression + { + $forbiddenNamespaceSelectors = array_filter( + array_map(function (string $componentName): ?string { + $selector = $this->componentSelectors[$componentName]; + + return \is_string($selector) ? $selector : null; + }, $forbiddenComponents) + ); + + $forbiddenExpressionSelectors = array_filter( + array_map(function (string $componentName): ?Expression { + $selector = $this->componentSelectors[$componentName]; + + return \is_string($selector) ? null : $selector; + }, $forbiddenComponents) + ); + + $forbiddenExpressionList = []; + if ([] !== $forbiddenNamespaceSelectors) { + $forbiddenExpressionList[] = new NotDependsOnTheseNamespaces(...$forbiddenNamespaceSelectors); + } + if ([] !== $forbiddenExpressionSelectors) { + $forbiddenExpressionList[] = new Not(new Andx(...$forbiddenExpressionSelectors)); + } + + return 1 === \count($forbiddenExpressionList) + ? array_pop($forbiddenExpressionList) + : new Andx(...$forbiddenExpressionList); + } } diff --git a/src/RuleBuilders/Architecture/DefinedBy.php b/src/RuleBuilders/Architecture/DefinedBy.php index a695f135..4bebf488 100644 --- a/src/RuleBuilders/Architecture/DefinedBy.php +++ b/src/RuleBuilders/Architecture/DefinedBy.php @@ -3,8 +3,13 @@ namespace Arkitect\RuleBuilders\Architecture; +use Arkitect\Expression\Expression; + interface DefinedBy { /** @return Component&Where */ public function definedBy(string $selector); + + /** @return Component&Where */ + public function definedByExpression(Expression $selector); } diff --git a/tests/Unit/Architecture/ArchitectureTest.php b/tests/Unit/Architecture/ArchitectureTest.php index b4f484f3..4c810be4 100644 --- a/tests/Unit/Architecture/ArchitectureTest.php +++ b/tests/Unit/Architecture/ArchitectureTest.php @@ -3,6 +3,8 @@ namespace Arkitect\Tests\Unit\Architecture; +use Arkitect\Expression\Boolean\Andx; +use Arkitect\Expression\Boolean\Not; use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces; use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces; use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces; @@ -39,6 +41,71 @@ public function test_layered_architecture(): void self::assertEquals($expectedRules, iterator_to_array($rules)); } + public function test_layered_architecture_with_expression(): void + { + $rules = Architecture::withComponents() + ->component('Domain')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Domain\*')) + ->component('Application')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Application\*')) + ->component('Infrastructure') + ->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*')) + + ->where('Domain')->shouldNotDependOnAnyComponent() + ->where('Application')->mayDependOnComponents('Domain') + ->where('Infrastructure')->mayDependOnAnyComponent() + + ->rules(); + + $expectedRules = [ + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*')) + ->should(new Not(new Andx( + new ResideInOneOfTheseNamespaces('App\*\Application\*'), + new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*') + ))) + ->because('of component architecture'), + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('App\*\Application\*')) + ->should(new Not(new Andx( + new ResideInOneOfTheseNamespaces('App\*\Infrastructure\*') + ))) + ->because('of component architecture'), + ]; + + self::assertEquals($expectedRules, iterator_to_array($rules)); + } + + public function test_layered_architecture_with_mix_of_namespace_and_expression(): void + { + $rules = Architecture::withComponents() + ->component('Domain')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Domain\*')) + ->component('Application')->definedByExpression(new ResideInOneOfTheseNamespaces('App\*\Application\*')) + ->component('Infrastructure')->definedBy('App\*\Infrastructure\*') + + ->where('Domain')->shouldNotDependOnAnyComponent() + ->where('Application')->mayDependOnComponents('Domain') + ->where('Infrastructure')->mayDependOnAnyComponent() + + ->rules(); + + $expectedRules = [ + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*')) + ->should(new Andx( + new NotDependsOnTheseNamespaces('App\*\Infrastructure\*'), + new Not(new Andx( + new ResideInOneOfTheseNamespaces('App\*\Application\*') + )) + )) + ->because('of component architecture'), + Rule::allClasses() + ->that(new ResideInOneOfTheseNamespaces('App\*\Application\*')) + ->should(new NotDependsOnTheseNamespaces('App\*\Infrastructure\*')) + ->because('of component architecture'), + ]; + + self::assertEquals($expectedRules, iterator_to_array($rules)); + } + public function test_layered_architecture_with_depends_only_on_components(): void { $rules = Architecture::withComponents()