*/ private const APPROVED_SUPPORT_TOP_LEVEL_ENTRIES = [ 'Exception', 'Util', ]; /** @var list */ private const APPROVED_SUPPORT_FILES = [ 'Exception/NotFoundException.php', 'Util/DateParser.php', 'Util/SlugHelper.php', ]; public function testSupportUsesOnlyApprovedTopLevelEntries(): void { $supportPath = $this->projectPath('src/Kernel/Support'); self::assertDirectoryExists($supportPath); $entries = array_values(array_filter(scandir($supportPath) ?: [], static fn (string $entry): bool => !in_array($entry, ['.', '..'], true))); sort($entries); self::assertSame( self::APPROVED_SUPPORT_TOP_LEVEL_ENTRIES, $entries, 'Kernel/Support is frozen by default: only explicitly approved subdirectories may exist.', ); } public function testSupportContainsOnlyExplicitlyApprovedFiles(): void { $supportPath = $this->projectPath('src/Kernel/Support'); $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($supportPath, \FilesystemIterator::SKIP_DOTS), ); $files = []; foreach ($iterator as $fileInfo) { if (!$fileInfo->isFile() || $fileInfo->getExtension() !== 'php') { continue; } $files[] = substr($fileInfo->getPathname(), strlen($supportPath) + 1); } sort($files); self::assertSame( self::APPROVED_SUPPORT_FILES, $files, 'Any new file under Kernel/Support must be a deliberate architectural decision: update the allow list, tests and documentation together.', ); } public function testSupportStaysIndependentFromFeatureModulesAndFrameworks(): void { $this->assertFilesDoNotReferenceFragments( $this->phpFilesUnder('src/Kernel/Support'), array_merge( self::APPLICATION_NAMESPACES, self::INFRASTRUCTURE_NAMESPACES, self::UI_NAMESPACES, self::FRAMEWORK_NAMESPACE_FRAGMENTS, self::HTTP_MESSAGE_DEPENDENCIES, ), 'Kernel/Support must stay pure, transverse and framework-agnostic', ); } public function testSupportClassesAreFinal(): void { foreach ($this->phpFilesUnder('src/Kernel/Support') as $file) { $code = $this->normalizedCode($file); self::assertTrue( str_contains($code, 'finalclass'), $this->fileRuleMessage('Kernel/Support classes must be final to avoid becoming extension points by accident', $file), ); } } }