Skip to content

Commit

Permalink
Create a IsA expression
Browse files Browse the repository at this point in the history
This will allow for testing if a class extends or
implements another code unit, anywhere in the inheritance tree.
  • Loading branch information
Herberto Graca committed Jul 26, 2023
1 parent 976c200 commit 7b00915
Show file tree
Hide file tree
Showing 7 changed files with 205 additions and 0 deletions.
56 changes: 56 additions & 0 deletions src/Expression/ForClasses/IsA.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

declare(strict_types=1);

namespace Arkitect\Expression\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Expression\Description;
use Arkitect\Expression\Expression;
use Arkitect\Rules\Violation;
use Arkitect\Rules\ViolationMessage;
use Arkitect\Rules\Violations;

final class IsA implements Expression
{
/** @var string[] */
private $allowedFqcnList;

public function __construct(string ...$allowedFqcnList)
{
$this->allowedFqcnList = $allowedFqcnList;
}

public function describe(ClassDescription $theClass, string $because): Description
{
$allowedFqcnList = implode(', ', $this->allowedFqcnList);

return new Description("should inherit from one of: $allowedFqcnList", $because);
}

public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
{
if (!$this->isA($theClass, ...$this->allowedFqcnList)) {
$violation = Violation::create(
$theClass->getFQCN(),
ViolationMessage::selfExplanatory($this->describe($theClass, $because))
);

$violations->add($violation);
}
}

private function isA(ClassDescription $theClass, string ...$allowedFqcnList): bool
{
foreach ($allowedFqcnList as $allowedFqcn) {
if (
\is_a($theClass->getFQCN(), $allowedFqcn, true)
|| \is_subclass_of($theClass->getFQCN(), $allowedFqcn)
) {
return true;
}
}

return false;
}
}
10 changes: 10 additions & 0 deletions tests/Unit/Expressions/ForClasses/DummyClasses/Banana.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

class Banana implements FruitInterface
{

}
10 changes: 10 additions & 0 deletions tests/Unit/Expressions/ForClasses/DummyClasses/CavendishBanana.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

class CavendishBanana extends Banana
{

}
10 changes: 10 additions & 0 deletions tests/Unit/Expressions/ForClasses/DummyClasses/Dog.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

final class Dog
{

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

final class DwarfCavendishBanana extends CavendishBanana
{

}
10 changes: 10 additions & 0 deletions tests/Unit/Expressions/ForClasses/DummyClasses/FruitInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses;

interface FruitInterface
{

}
99 changes: 99 additions & 0 deletions tests/Unit/Expressions/ForClasses/IsATest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace Arkitect\Tests\Unit\Expressions\ForClasses;

use Arkitect\Analyzer\ClassDescription;
use Arkitect\Analyzer\FullyQualifiedClassName;
use Arkitect\Expression\ForClasses\IsA;
use Arkitect\Rules\Violations;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\Banana;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\CavendishBanana;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\Dog;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\DwarfCavendishBanana;
use Arkitect\Tests\Unit\Expressions\ForClasses\DummyClasses\FruitInterface;
use PHPUnit\Framework\TestCase;

final class IsATest extends TestCase
{
/**
* @test
*/
public function it_should_have_no_violation_when_it_implements(): void
{
$interface = FruitInterface::class;
$isA = new IsA($interface);
$classDescription = new ClassDescription(
FullyQualifiedClassName::fromString(CavendishBanana::class),
[],
[FullyQualifiedClassName::fromString($interface)],
null,
false,
false,
false,
false,
false
);

$violations = new Violations();
$isA->evaluate($classDescription, $violations, '');

self::assertEquals(0, $violations->count());
}

/**
* @test
*/
public function it_should_have_no_violation_when_it_extends(): void
{
$class = Banana::class;
$isA = new IsA($class);
$classDescription = new ClassDescription(
FullyQualifiedClassName::fromString(DwarfCavendishBanana::class),
[],
[],
FullyQualifiedClassName::fromString($class),
false,
false,
false,
false,
false
);

$violations = new Violations();
$isA->evaluate($classDescription, $violations, '');

self::assertEquals(0, $violations->count());
}

/**
* @test
*/
public function it_should_have_violation_if_it_doesnt_extend_nor_implement(): void
{
$interface = FruitInterface::class;
$class = Banana::class;
$isA = new IsA($class, $interface);
$classDescription = new ClassDescription(
FullyQualifiedClassName::fromString(Dog::class),
[],
[],
null,
false,
false,
false,
false,
false
);

$violations = new Violations();
$isA->evaluate($classDescription, $violations, '');

self::assertEquals(1, $violations->count());
self::assertEquals(
"should inherit from one of: $class, $interface",
$isA->describe($classDescription, '')->toString()
);
}
}

0 comments on commit 7b00915

Please sign in to comment.