Skip to content

Commit

Permalink
Allow architectures to be created from expressions
Browse files Browse the repository at this point in the history
This will allow for composable and more complex rules.
  • Loading branch information
Herberto Graca committed Jul 29, 2023
1 parent b643ece commit 38d4a4a
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 6 deletions.
47 changes: 41 additions & 6 deletions src/RuleBuilders/Architecture/Architecture.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}
}
Expand All @@ -115,4 +121,33 @@ 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 count($forbiddenExpressionList) === 1
? array_pop($forbiddenExpressionList)
: new Andx(...$forbiddenExpressionList);
}
}
5 changes: 5 additions & 0 deletions src/RuleBuilders/Architecture/DefinedBy.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
67 changes: 67 additions & 0 deletions tests/Unit/Architecture/ArchitectureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit 38d4a4a

Please sign in to comment.