Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,13 @@ return static function (Config $config): void {
->component('Service')->definedBy('App\Service\*')
->component('Repository')->definedBy('App\Repository\*')
->component('Entity')->definedBy('App\Entity\*')
->component('Domain')->definedBy('App\Domain\*')

->where('Controller')->mayDependOnComponents('Service', 'Entity')
->where('Service')->mayDependOnComponents('Repository', 'Entity')
->where('Repository')->mayDependOnComponents('Entity')
->where('Entity')->shouldNotDependOnAnyComponent()
->where('Domain')->shouldOnlyDependOnComponents('Domain')

->rules();

Expand Down
37 changes: 30 additions & 7 deletions src/RuleBuilders/Architecture/Architecture.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,28 @@

namespace Arkitect\RuleBuilders\Architecture;

use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\Rules\Rule;

class Architecture implements Component, DefinedBy, Where, MayDependOnComponents, MayDependOnAnyComponent, ShouldNotDependOnAnyComponent, Rules
class Architecture implements Component, DefinedBy, Where, MayDependOnComponents, MayDependOnAnyComponent, ShouldNotDependOnAnyComponent, ShouldOnlyDependOnComponents, Rules
{
/** @var string */
private $componentName;
/** @var array<string, string> */
private $componentSelectors;
/** @var array<string, string[]> */
private $allowedDependencies;
/** @var array<string, string[]> */
private $componentDependsOnlyOnTheseNamespaces;

private function __construct()
{
$this->componentName = '';
$this->componentSelectors = [];
$this->allowedDependencies = [];
$this->componentDependsOnlyOnTheseNamespaces = [];
}

public static function withComponents(): Component
Expand All @@ -38,7 +42,6 @@ public function component(string $name): DefinedBy
public function definedBy(string $selector)
{
$this->componentSelectors[$this->componentName] = $selector;
$this->allowedDependencies[$this->componentName] = [];

return $this;
}
Expand All @@ -57,6 +60,13 @@ public function shouldNotDependOnAnyComponent()
return $this;
}

public function shouldOnlyDependOnComponents(string ...$componentNames)
{
$this->componentDependsOnlyOnTheseNamespaces[$this->componentName] = $componentNames;

return $this;
}

public function mayDependOnComponents(string ...$componentNames)
{
$this->allowedDependencies[$this->componentName] = $componentNames;
Expand All @@ -76,19 +86,32 @@ public function rules(): iterable
$layerNames = array_keys($this->componentSelectors);

foreach ($this->componentSelectors as $name => $selector) {
$forbiddenComponents = array_diff($layerNames, [$name], $this->allowedDependencies[$name]);
if (isset($this->allowedDependencies[$name])) {
$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))
->because('of component architecture');
}
}

if (empty($forbiddenComponents)) {
if (!isset($this->componentDependsOnlyOnTheseNamespaces[$name])) {
continue;
}

$forbiddenSelectors = array_map(function (string $componentName): string {
$allowedDependencies = array_map(function (string $componentName): string {
return $this->componentSelectors[$componentName];
}, $forbiddenComponents);
}, $this->componentDependsOnlyOnTheseNamespaces[$name]);

yield Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces($selector))
->should(new NotDependsOnTheseNamespaces(...$forbiddenSelectors))
->should(new DependsOnlyOnTheseNamespaces(...$allowedDependencies))
->because('of component architecture');
}
}
Expand Down
10 changes: 10 additions & 0 deletions src/RuleBuilders/Architecture/ShouldOnlyDependOnComponents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
declare(strict_types=1);

namespace Arkitect\RuleBuilders\Architecture;

interface ShouldOnlyDependOnComponents
{
/** @return Where&Rules */
public function shouldOnlyDependOnComponents(string ...$componentNames);
}
2 changes: 1 addition & 1 deletion src/RuleBuilders/Architecture/Where.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@

interface Where
{
/** @return ShouldNotDependOnAnyComponent&MayDependOnComponents&MayDependOnAnyComponent */
/** @return ShouldNotDependOnAnyComponent&ShouldOnlyDependOnComponents&MayDependOnComponents&MayDependOnAnyComponent */
public function where(string $componentName);
}
19 changes: 19 additions & 0 deletions tests/Unit/Architecture/ArchitectureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace Arkitect\Tests\Unit\Architecture;

use Arkitect\Expression\ForClasses\DependsOnlyOnTheseNamespaces;
use Arkitect\Expression\ForClasses\NotDependsOnTheseNamespaces;
use Arkitect\Expression\ForClasses\ResideInOneOfTheseNamespaces;
use Arkitect\RuleBuilders\Architecture\Architecture;
Expand Down Expand Up @@ -37,4 +38,22 @@ public function test_layered_architecture(): void

self::assertEquals($expectedRules, iterator_to_array($rules));
}

public function test_layered_architecture_with_depends_only_on_components(): void
{
$rules = Architecture::withComponents()
->component('Domain')->definedBy('App\*\Domain\*')
->where('Domain')->shouldOnlyDependOnComponents('Domain')

->rules();

$expectedRules = [
Rule::allClasses()
->that(new ResideInOneOfTheseNamespaces('App\*\Domain\*'))
->should(new DependsOnlyOnTheseNamespaces('App\*\Domain\*'))
->because('of component architecture'),
];

self::assertEquals($expectedRules, iterator_to_array($rules));
}
}