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)); } } }