94 lines
3.7 KiB
PHP
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));
|
|
}
|
|
}
|
|
}
|