diff --git a/src/Ticket/TicketRepository-traits/TicketQuery.php b/src/Ticket/TicketQuery.php similarity index 100% rename from src/Ticket/TicketRepository-traits/TicketQuery.php rename to src/Ticket/TicketQuery.php diff --git a/src/Ticket/TicketRepository.php b/src/Ticket/TicketRepository.php index 40fa4f88..fa702b66 100644 --- a/src/Ticket/TicketRepository.php +++ b/src/Ticket/TicketRepository.php @@ -133,6 +133,8 @@ final class TicketRepository { /* ------------------------------------------------------------------ * * 状态机(对外入口,内部规则在 TicketStatus trait) + * + * Trait 必须与宿主类同级平铺:PSR-4 按命名空间寻址,不解析子目录。 * ------------------------------------------------------------------ */ /** diff --git a/src/Ticket/TicketRepository-traits/TicketStatus.php b/src/Ticket/TicketStatus.php similarity index 100% rename from src/Ticket/TicketRepository-traits/TicketStatus.php rename to src/Ticket/TicketStatus.php diff --git a/tools/audit-psr4-paths.php b/tools/audit-psr4-paths.php new file mode 100644 index 00000000..ec6d2ff8 --- /dev/null +++ b/tools/audit-psr4-paths.php @@ -0,0 +1,76 @@ + 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 = 'AKSRT'; + +$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);