#!/usr/bin/env php */ const DOMAIN_NS = [ 'admin' => 'Admin', 'ai' => 'Ai', 'auth' => 'Auth', 'content' => 'Content', 'core' => 'Core', 'credits' => 'Credits', 'extensions' => 'Extensions', 'forum' => 'Forum', 'frontend' => 'Frontend', 'messages' => 'Messages', 'payment' => 'Payment', 'settings' => 'Settings', 'shop' => 'Shop', 'sms' => 'Sms', 'user' => 'User', 'widgets' => 'Widgets', ]; /** * Explicit short-name overrides (old global → short class under domain). * Used for reserved words / clarity. * * @var array */ const SHORT_OVERRIDES = [ 'AKSRT_Admin_Schema_Global' => 'GlobalSchema', 'AKSRT_Payment_Gateway_Interface' => 'PaymentGatewayInterface', 'AKSRT_SMS_Provider' => 'SmsProviderInterface', // Preserve brand/acronym casing in short names after segment join. 'AKSRT_OAuth' => 'OAuth', 'AKSRT_OAuth_Part_1' => 'OAuthPart1', 'AKSRT_OAuth_Part_2' => 'OAuthPart2', 'AKSRT_OAuth_Proxy_Client' => 'OAuthProxyClient', 'AKSRT_OAuth_Proxy_Server' => 'OAuthProxyServer', 'AKSRT_WeChat_Notify' => 'WeChatNotify', 'AKSRT_WechatPay_Gateway' => 'WechatPayGateway', 'AKSRT_PayPal_Gateway' => 'PayPalGateway', 'AKSRT_MeiliSearch' => 'MeiliSearch', 'AKSRT_Custom_HTML_Gap_Widget' => 'CustomHtmlGapWidget', ]; /** * Forced domain for helpers moved into the tree. * * @var array */ const HELPER_DOMAIN = [ 'AKSRT_Nav_Walker' => 'Frontend', 'AKSRT_Shortcodes' => 'Core', 'AKSRT_Domain_Model' => 'Core', ]; /** * Convert AKSRT_Foo_Bar_2 → FooBar2 (preserve short ALLCAPS tokens like AI, SEO, SMS, DB, IP, API). */ function aksrt_to_short(string $old): string { if (isset(SHORT_OVERRIDES[$old])) { return SHORT_OVERRIDES[$old]; } if (!str_starts_with($old, 'AKSRT_')) { throw new RuntimeException("Unexpected name: {$old}"); } $parts = explode('_', substr($old, 6)); $out = ''; foreach ($parts as $p) { if ($p === '') { continue; } if (preg_match('/^\d+$/', $p)) { $out .= $p; continue; } // Keep short uppercase acronyms as-is. if (preg_match('/^[A-Z]{2,5}$/', $p)) { $out .= $p; continue; } $out .= ucfirst(strtolower($p)); } return $out; } /** * Nested path segments under domain → extra namespace segments. * * @return list */ function aksrt_path_ns_segments(string $rel_under_domain): array { $rel_under_domain = str_replace('\\', '/', $rel_under_domain); if ($rel_under_domain === '' || $rel_under_domain === '.') { return []; } $parts = array_values(array_filter(explode('/', $rel_under_domain), static fn($p) => $p !== '' && $p !== '.')); // Drop filename. array_pop($parts); $segs = []; foreach ($parts as $p) { // Trait dump folders → no extra NS (class short name already unique). if (str_ends_with($p, '-traits') || $p === 'user-parts') { continue; } if ($p === 'gateways') { $segs[] = 'Gateways'; continue; } if ($p === 'schema') { $segs[] = 'Schema'; continue; } if ($p === 'post-list') { $segs[] = 'PostList'; continue; } if ($p === 'native') { $segs[] = 'Native'; continue; } // Unknown nested folder: PascalCase the slug. $segs[] = str_replace(' ', '', ucwords(str_replace(['-', '_'], ' ', $p))); } return $segs; } /** * Scan PHP files for top-level type declarations. * * @return list */ function aksrt_scan_types(string $root): array { $out = []; $dirs = [ $root . '/inc/classes', ]; $extra_files = [ $root . '/inc/helpers/class-aksrt-nav-walker.php', $root . '/inc/helpers/aksrt-shortcodes.php', $root . '/inc/helpers/aksrt-domain-model.php', ]; $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dirs[0])); $files = []; foreach ($rii as $file) { if ($file->isFile() && str_ends_with($file->getFilename(), '.php')) { $files[] = $file->getPathname(); } } foreach ($extra_files as $f) { if (is_file($f)) { $files[] = $f; } } $re = '/^(?:abstract\s+|final\s+)?(class|trait|interface)\s+(\w+)/m'; foreach ($files as $abs) { $rel = str_replace('\\', '/', substr($abs, strlen($root) + 1)); $text = file_get_contents($abs); if ($text === false) { continue; } if (!preg_match_all($re, $text, $m, PREG_SET_ORDER)) { $out[] = [ 'kind' => 'procedural', 'name' => null, 'path' => $rel, ]; continue; } foreach ($m as $match) { $out[] = [ 'kind' => $match[1], 'name' => $match[2], 'path' => $rel, ]; } } return $out; } /** * @param list $scanned * @return array{entries: list>, procedural: list, errors: list} */ function aksrt_build_map(array $scanned, string $root): array { $entries = []; $procedural = []; $errors = []; $by_old = []; foreach ($scanned as $row) { if ($row['kind'] === 'procedural') { $procedural[] = $row['path']; continue; } $old = $row['name']; $path = $row['path']; $kind = $row['kind']; if (!str_starts_with($old, 'AKSRT_') && $old !== 'AKSRT') { $errors[] = "Non-AKSRT type {$old} in {$path}"; continue; } // Domain $domain_ns = null; $rel_under = ''; if (str_starts_with($path, 'inc/helpers/')) { $domain_ns = HELPER_DOMAIN[$old] ?? 'Core'; $rel_under = ''; } elseif (preg_match('#^inc/classes/([^/]+)(?:/(.*))?$#', $path, $pm)) { $dom = $pm[1]; if (!isset(DOMAIN_NS[$dom])) { $errors[] = "Unknown domain folder {$dom} for {$path}"; continue; } $domain_ns = DOMAIN_NS[$dom]; $rel_under = $pm[2] ?? ''; } else { $errors[] = "Unmapped path {$path}"; continue; } $extra = aksrt_path_ns_segments($rel_under); $short = aksrt_to_short($old); // Strip redundant leading domain token from short name when not emptying it. // AKSRT_Admin_AI_Controller → AdminAIController → AIController under Admin\. if (str_starts_with($short, $domain_ns) && strlen($short) > strlen($domain_ns)) { $rest = substr($short, strlen($domain_ns)); if ($rest !== '' && preg_match('/^[A-Z]/', $rest)) { $short = $rest; } } // PSR-4 root: src/AKSRT/ (avoids Windows case-collision with inc/classes/admin vs Admin). $ns_parts = array_merge(['AKSRT', $domain_ns], $extra); $fqcn = implode('\\', $ns_parts) . '\\' . $short; $new_rel = 'src/AKSRT/' . implode('/', array_slice($ns_parts, 1)) . '/' . $short . '.php'; $entry = [ 'kind' => $kind, 'old' => $old, 'new_fqcn' => $fqcn, 'namespace' => implode('\\', $ns_parts), 'short' => $short, 'old_path' => $path, 'new_path' => $new_rel, 'multi_source' => false, ]; if (isset($by_old[$old])) { $errors[] = "Duplicate old name {$old}: {$by_old[$old]['old_path']} vs {$path}"; } $by_old[$old] = $entry; $entries[] = $entry; } // Mark multi-decl sources. $by_path = []; foreach ($entries as $i => $e) { $by_path[$e['old_path']][] = $i; } foreach ($by_path as $path => $idxs) { if (count($idxs) > 1) { foreach ($idxs as $i) { $entries[$i]['multi_source'] = true; } } } // Detect FQCN collisions. $by_new = []; foreach ($entries as $e) { $k = $e['new_fqcn']; if (isset($by_new[$k])) { $errors[] = "FQCN collision {$k}: {$by_new[$k]} vs {$e['old']}"; } $by_new[$k] = $e['old']; } // Detect new_path collisions (two types → same file). $by_new_path = []; foreach ($entries as $e) { $k = $e['new_path']; if (isset($by_new_path[$k])) { $errors[] = "Path collision {$k}: {$by_new_path[$k]} vs {$e['old']}"; } $by_new_path[$k] = $e['old']; } return [ 'entries' => $entries, 'procedural' => $procedural, 'errors' => $errors, 'by_old' => $by_old, ]; } $scanned = aksrt_scan_types($root); $map = aksrt_build_map($scanned, $root); $summary = [ 'generated_at' => gmdate('c'), 'counts' => [ 'types' => count($map['entries']), 'classes' => count(array_filter($map['entries'], static fn($e) => $e['kind'] === 'class')), 'traits' => count(array_filter($map['entries'], static fn($e) => $e['kind'] === 'trait')), 'interfaces' => count(array_filter($map['entries'], static fn($e) => $e['kind'] === 'interface')), 'procedural_files' => count($map['procedural']), 'multi_source_types' => count(array_filter($map['entries'], static fn($e) => $e['multi_source'])), 'errors' => count($map['errors']), ], 'procedural' => $map['procedural'], 'errors' => $map['errors'], 'entries' => $map['entries'], ]; echo "Types: {$summary['counts']['types']} (class={$summary['counts']['classes']} trait={$summary['counts']['traits']} iface={$summary['counts']['interfaces']})\n"; echo "Procedural files: {$summary['counts']['procedural_files']}\n"; echo "Multi-source types: {$summary['counts']['multi_source_types']}\n"; echo "Errors: {$summary['counts']['errors']}\n"; if ($map['errors']) { foreach ($map['errors'] as $err) { echo " ERROR: {$err}\n"; } } // Sample print. foreach (array_slice($map['entries'], 0, 8) as $e) { echo " {$e['old']} => {$e['new_fqcn']}\n {$e['old_path']} -> {$e['new_path']}\n"; } if ($write) { $dir = $root . '/tools/psr4-map'; if (!is_dir($dir) && !mkdir($dir, 0777, true) && !is_dir($dir)) { fwrite(STDERR, "Cannot create {$dir}\n"); exit(1); } $file = $dir . '/map.json'; file_put_contents($file, json_encode($summary, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"); // Also CSV for quick review. $csv = $dir . '/map.csv'; $fh = fopen($csv, 'wb'); fputcsv($fh, ['kind', 'old', 'new_fqcn', 'old_path', 'new_path', 'multi_source'], ',', '"', '\\'); foreach ($map['entries'] as $e) { fputcsv($fh, [$e['kind'], $e['old'], $e['new_fqcn'], $e['old_path'], $e['new_path'], $e['multi_source'] ? '1' : '0'], ',', '"', '\\'); } fclose($fh); echo "Wrote {$file} and {$csv}\n"; } exit($map['errors'] ? 1 : 0);