Files
netslim-core/tests/Architecture/ApplicationWorkflowShapeTest.php
2026-03-20 22:13:41 +01:00

94 lines
3.7 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Architecture;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use Tests\Architecture\Support\ArchitectureTestCase;
final class ApplicationWorkflowShapeTest extends ArchitectureTestCase
{
public function testUseCasesFollowTheProjectShapeConvention(): void
{
$files = $this->phpFilesUnder('src/*/Application/UseCase');
self::assertNotSame([], $files, 'Expected at least one use case file.');
foreach ($files as $file) {
$class = $this->reflectProjectClass($file);
$basename = pathinfo($file, PATHINFO_FILENAME);
self::assertSame(
$basename,
$class->getShortName(),
$this->fileRuleMessage('Use cases must be named after their file', $file),
);
self::assertTrue($class->isFinal(), $this->fileRuleMessage('Use cases must be final classes', $file));
self::assertTrue($class->isReadOnly(), $this->fileRuleMessage('Use cases must be readonly classes', $file));
$publicMethods = array_values(array_map(
static fn (ReflectionMethod $method): string => $method->getName(),
array_filter(
$class->getMethods(ReflectionMethod::IS_PUBLIC),
static fn (ReflectionMethod $method): bool => $method->getDeclaringClass()->getName() === $class->getName()
&& !$method->isConstructor()
&& !$method->isDestructor(),
),
));
sort($publicMethods);
self::assertSame(
['handle'],
$publicMethods,
$this->fileRuleMessage('Use cases must expose a single public handle(...) entrypoint', $file),
);
}
}
public function testCommandsRemainImmutableInputDtos(): void
{
$files = $this->phpFilesUnder('src/*/Application/Command');
self::assertNotSame([], $files, 'Expected at least one command file.');
foreach ($files as $file) {
$basename = pathinfo($file, PATHINFO_FILENAME);
$class = $this->reflectProjectClass($file);
self::assertStringEndsWith('Command', $basename, $this->fileRuleMessage('Command files must end with Command', $file));
self::assertSame(
$basename,
$class->getShortName(),
$this->fileRuleMessage('Commands must be named after their file', $file),
);
self::assertTrue($class->isFinal(), $this->fileRuleMessage('Commands must be final classes', $file));
self::assertTrue($class->isReadOnly(), $this->fileRuleMessage('Commands must be readonly classes', $file));
}
}
public function testUiLayerDoesNotReferenceUseCasesOrCommandsDirectly(): void
{
$files = $this->uiFiles();
self::assertNotSame([], $files, 'Expected UI files to exist.');
$this->assertFilesDoNotReferenceFragments(
$files,
['\\Application\\UseCase\\', '\\Application\\Command\\'],
'UI layer must enter a domain through its application service, not through use cases or commands',
);
}
private function reflectProjectClass(string $file): ReflectionClass
{
$relativePath = $this->relativePath($file);
$className = 'Netig\Netslim\\' . str_replace(['/', '.php'], ['\\', ''], substr($relativePath, 4));
try {
return new ReflectionClass($className);
} catch (ReflectionException $exception) {
self::fail($this->fileRuleMessage('Unable to reflect class ' . $className . ': ' . $exception->getMessage(), $file));
}
}
}