*/ private const EXCLUDED_CLASSES = [ 'MintyPHP\\Repository\\Content\\PageContentRepository', 'MintyPHP\\Repository\\Content\\PageRepository', 'MintyPHP\\Repository\\CustomField\\TenantCustomFieldDefinitionRepository', 'MintyPHP\\Repository\\CustomField\\TenantCustomFieldOptionRepository', 'MintyPHP\\Repository\\CustomField\\UserCustomFieldValueRepository', 'MintyPHP\\Repository\\CustomField\\UserCustomFieldValueOptionRepository', 'MintyPHP\\Repository\\Support\\RepoQuery', ]; /** * @return array */ private function collectRepositoryPairs(): array { $root = $this->projectRootPath(); $repoDir = $root . '/core/Repository'; $pairs = []; $iterator = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator($repoDir, \RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { /** @var \SplFileInfo $file */ if ($file->getExtension() !== 'php') { continue; } $filename = $file->getFilename(); // Only concrete repository files (not interfaces, not utilities) if (!str_ends_with($filename, 'Repository.php')) { continue; } if (str_ends_with($filename, 'RepositoryInterface.php')) { continue; } // Derive PSR-4 class name: core/Repository/Foo/Bar.php → MintyPHP\Repository\Foo\Bar $relativePath = ltrim(str_replace($root, '', $file->getPathname()), '/'); $className = 'MintyPHP\\' . str_replace(['/', '.php'], ['\\', ''], substr($relativePath, strlen('core/'))); if (in_array($className, self::EXCLUDED_CLASSES, true)) { continue; } $interfaceClass = $className . 'Interface'; $interfaceFile = substr($relativePath, 0, -strlen('.php')) . 'Interface.php'; $pairs[$className] = [ 'class' => $className, 'interfaceClass' => $interfaceClass, 'interfaceFile' => $interfaceFile, ]; } ksort($pairs); return $pairs; } public function testEveryRepositoryHasACoLocatedInterface(): void { $pairs = $this->collectRepositoryPairs(); $this->assertNotEmpty($pairs, 'No repository classes found — check path resolution.'); $missing = []; foreach ($pairs as $className => $pair) { $root = $this->projectRootPath(); if (!file_exists($root . '/' . $pair['interfaceFile'])) { $missing[] = $pair['interfaceFile'] . ' (for ' . $className . ')'; } } $this->assertSame( [], $missing, "Missing interface files:\n" . implode("\n", $missing) ); } public function testEveryRepositoryImplementsItsInterface(): void { $pairs = $this->collectRepositoryPairs(); $violations = []; foreach ($pairs as $className => $pair) { if (!class_exists($className)) { $violations[] = $className . ' — class not found (autoload issue?)'; continue; } $implemented = class_implements($className); if ($implemented === false || !in_array($pair['interfaceClass'], $implemented, true)) { $violations[] = $className . ' does not implement ' . $pair['interfaceClass']; } } $this->assertSame( [], $violations, "Repository interface contract violations:\n" . implode("\n", $violations) ); } }