src/` (composer.json) — no bootstrap require list — so a misplaced * file loads nowhere and fatals the moment its host class compiles. * * Usage (from theme root): * php tools/audit-psr4-paths.php * * Exit 1 on hard errors. */ declare(strict_types=1); $themeRoot = dirname(__DIR__); $srcRoot = $themeRoot . '/src'; $prefix = 'Jio'; $rii = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($srcRoot, FilesystemIterator::SKIP_DOTS) ); $files = []; foreach ($rii as $file) { if ($file->isFile() && strtolower($file->getExtension()) === 'php') { $files[] = $file->getPathname(); } } sort($files); $problems = []; foreach ($files as $path) { $src = (string) file_get_contents($path); if (!preg_match('/^\s*namespace\s+([^;]+);/m', $src, $nsMatch)) { continue; } $namespace = trim($nsMatch[1]); if (!str_starts_with($namespace, $prefix . '\\')) { continue; } if (!preg_match('/^\s*(?:final\s+|abstract\s+|readonly\s+)*(?:class|interface|trait|enum)\s+(\w+)/m', $src, $symMatch)) { continue; } $symbol = $symMatch[1]; $relative = str_replace('\\', '/', substr($namespace, strlen($prefix))); $expected = str_replace('//', '/', $srcRoot . $relative . '/' . $symbol . '.php'); if (realpath($expected) !== realpath($path)) { $problems[] = sprintf( "%s declares %s\\%s -> expected %s", str_replace('\\', '/', substr($path, strlen($srcRoot) + 1)), $namespace, $symbol, str_replace('\\', '/', substr($expected, strlen($srcRoot) + 1)) ); } } foreach ($problems as $problem) { fwrite(STDERR, '[error] ' . $problem . PHP_EOL); } fwrite(STDERR, sprintf( 'scanned=%d errors=%d%s', count($files), count($problems), PHP_EOL )); exit($problems === [] ? 0 : 1);