diff --git a/tools/adr-classes-domain-placement.md b/tools/adr-classes-domain-placement.md index 4e2546f7..9c27987a 100644 --- a/tools/adr-classes-domain-placement.md +++ b/tools/adr-classes-domain-placement.md @@ -53,8 +53,8 @@ Domain folders were frozen after the root→domain move. Theme types then migrat ### Map tooling -- `php tools/build-psr4-map.php --write` — inventory / map under `tools/psr4-map/` -- Migration helpers under `tools/psr4-*.php` (historical; re-run only with care) +- `php tools/build-psr4-map.php --write` — read-only inventory / map under `tools/psr4-map/` +- One-shot mass-rewrite migrators are **not** kept in-tree (completed migration; AV false positives on file-tree rewriters) ## Out of scope diff --git a/tools/psr4-fix-multi.php b/tools/psr4-fix-multi.php deleted file mode 100644 index 5169cf90..00000000 --- a/tools/psr4-fix-multi.php +++ /dev/null @@ -1,215 +0,0 @@ - - */ -function list_types(string $text): array { - $out = []; - if (!preg_match_all( - '/^(?:abstract\s+|final\s+)?(class|trait|interface)\s+(\w+)/m', - $text, - $m, - PREG_SET_ORDER | PREG_OFFSET_CAPTURE - )) { - return []; - } - foreach ($m as $match) { - $decl = (int) $match[0][1]; - $kind = $match[1][0]; - $name = $match[2][0]; - // Include preceding docblock. - $before = substr($text, 0, $decl); - $start = $decl; - if (preg_match('/(?:\n|^)(\s*\/\*\*[\s\S]*?\*\/\s*)\z/', $before, $dm, PREG_OFFSET_CAPTURE)) { - $start = (int) $dm[1][1]; - } - $out[] = ['name' => $name, 'kind' => $kind, 'start' => $start, 'decl' => $decl]; - } - return $out; -} - -function extract_by_index(string $text, array $types, int $idx): string { - $start = $types[$idx]['start']; - // End is start of next type or EOF (but need end of braces of this type). - $brace = strpos($text, '{', $types[$idx]['decl']); - $len = strlen($text); - $depth = 0; - $in_str = false; - $str_q = ''; - $in_line = false; - $in_block = false; - $escape = false; - for ($i = $brace; $i < $len; $i++) { - $ch = $text[$i]; - $next = $i + 1 < $len ? $text[$i + 1] : ''; - if ($in_line) { - if ($ch === "\n") { - $in_line = false; - } - continue; - } - if ($in_block) { - if ($ch === '*' && $next === '/') { - $in_block = false; - $i++; - } - continue; - } - if ($in_str) { - if ($escape) { - $escape = false; - continue; - } - if ($ch === '\\') { - $escape = true; - continue; - } - if ($ch === $str_q) { - $in_str = false; - } - continue; - } - if ($ch === '/' && $next === '/') { - $in_line = true; - $i++; - continue; - } - if ($ch === '/' && $next === '*') { - $in_block = true; - $i++; - continue; - } - if ($ch === '"' || $ch === "'") { - $in_str = true; - $str_q = $ch; - continue; - } - if ($ch === '{') { - $depth++; - } elseif ($ch === '}') { - $depth--; - if ($depth === 0) { - return substr($text, $start, $i - $start + 1); - } - } - } - throw new RuntimeException('brace match failed for ' . $types[$idx]['name']); -} - -$rep = []; -foreach ($map['entries'] as $e) { - $rep[$e['old']] = '\\' . $e['new_fqcn']; -} -$rep['AKSRT_Creator_Revenue'] = '\\AKSRT\\Payment\\CreatorRevenue'; -uksort($rep, static fn($a, $b) => strlen($b) <=> strlen($a)); - -function rewrite_refs(string $code, array $rep): string { - $preserve = [ - 'AKSRT_DIR', 'AKSRT_URI', 'AKSRT_VERSION', 'AKSRT_NO_EMAIL_GATE', - 'AKSRT_PAY_TYPE_NO', 'AKSRT_PAY_TYPE_READING', 'AKSRT_PAY_TYPE_DOWNLOAD', - 'AKSRT_PAY_TYPE_GALLERY', 'AKSRT_PAY_TYPE_VIDEO', 'AKSRT_PAY_TYPE_VIP', - ]; - foreach ($preserve as $i => $tok) { - $code = str_replace($tok, "\0P{$i}\0", $code); - } - $pattern = '/\b(' . implode('|', array_map(static fn($k) => preg_quote($k, '/'), array_keys($rep))) . ')\b/'; - $code = preg_replace_callback($pattern, static fn($m) => $rep[$m[1]] ?? $m[1], $code) ?? $code; - foreach ($preserve as $i => $tok) { - $code = str_replace("\0P{$i}\0", $tok, $code); - } - return $code; -} - -$files = [ - 'inc/classes/payment/class-aksrt-payment-gateway.php', - 'inc/classes/widgets/class-aksrt-forum-widgets.php', - 'inc/classes/widgets/class-aksrt-home-widgets.php', - 'inc/classes/widgets/class-aksrt-shop-widgets.php', - 'inc/classes/widgets/class-aksrt-widgets-batch1.php', - 'inc/classes/widgets/class-aksrt-widgets-new.php', - 'inc/classes/widgets/class-aksrt-widgets-original.php', -]; - -foreach ($files as $git_path) { - $text = shell_exec('git -C ' . escapeshellarg($root) . ' show HEAD:' . escapeshellarg($git_path)); - if (!$text) { - fwrite(STDERR, "git show fail $git_path\n"); - continue; - } - $types = list_types($text); - echo $git_path, ' has ', count($types), " types: ", implode(', ', array_column($types, 'name')), "\n"; - foreach ($types as $idx => $info) { - $old = $info['name']; - if (!isset($by_old[$old])) { - fwrite(STDERR, " unmapped $old\n"); - continue; - } - $entry = $by_old[$old]; - $chunk = extract_by_index($text, $types, $idx); - // Rename: only the declaration line's name. - $chunk = preg_replace( - '/^((?:abstract\s+|final\s+)?' . preg_quote($info['kind'], '/') . '\s+)' . preg_quote($old, '/') . '\b/m', - '$1' . $entry['short'], - $chunk, - 1 - ); - $chunk = rewrite_refs($chunk, $rep); - $ns = $entry['namespace']; - // Shorten same-namespace refs BUT not if it would re-break declaration - declaration already short. - $chunk = preg_replace('/\\\\' . preg_quote($ns, '/') . '\\\\(\w+)/', '$1', $chunk) ?? $chunk; - // Global WP types. - foreach (['WP_Widget', 'WP_REST_Request', 'WP_REST_Response', 'WP_Error', 'WP_Post', 'WP_User', 'WP_Query'] as $g) { - $chunk = preg_replace('/(? {$entry['short']}\n"); - fwrite(STDERR, " head: " . substr($out, 0, 250) . "\n"); - continue; - } - $path = $root . '/' . $entry['new_path']; - if (!is_dir(dirname($path))) { - mkdir(dirname($path), 0777, true); - } - file_put_contents($path, $out); - echo " OK {$old} => {$entry['new_path']} (", strlen($out), " bytes)\n"; - } -} - -// Globalize WP_* in all src files once more carefully -$rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root . '/src/AKSRT')); -foreach ($rii as $f) { - if (!$f->isFile() || !str_ends_with($f->getFilename(), '.php')) { - continue; - } - $p = $f->getPathname(); - $t = (string) file_get_contents($p); - $orig = $t; - foreach (['WP_REST_Request', 'WP_REST_Response', 'WP_Error', 'WP_Post', 'WP_User', 'WP_Query', 'WP_Widget', 'WP_Admin_Bar', 'WP_HTTP_Response', 'WP_Term'] as $g) { - // only when used as type/extends/new/catch - simple: bare WP_ not already escaped - $t = preg_replace('/(?>, procedural: list} $map */ -$map = json_decode((string) file_get_contents($map_file), true, 512, JSON_THROW_ON_ERROR); - -const PRESERVE_TOKENS = [ - 'AKSRT_DIR', - 'AKSRT_URI', - 'AKSRT_VERSION', - 'AKSRT_NO_EMAIL_GATE', -]; - -/** - * Find declaration start offset for a type name. - */ -function aksrt_find_decl_start(string $text, string $old_name): ?int { - if (!preg_match( - '/(?:abstract\s+|final\s+)?(?:class|trait|interface)\s+' . preg_quote($old_name, '/') . '\b/', - $text, - $m, - PREG_OFFSET_CAPTURE - )) { - return null; - } - $decl = (int) $m[0][1]; - // Include immediately preceding docblock if present. - $before = substr($text, 0, $decl); - if (preg_match('/(?:\n|^)\s*\/\*\*[\s\S]*?\*\/\s*\z/', $before, $dm, PREG_OFFSET_CAPTURE)) { - return (int) $dm[0][1]; - } - return $decl; -} - -/** - * Extract one class/trait/interface by brace matching from its declaration. - */ -function aksrt_extract_type(string $text, string $old_name): ?string { - $start = aksrt_find_decl_start($text, $old_name); - if ($start === null) { - return null; - } - - $brace = strpos($text, '{', $start); - if ($brace === false) { - // Possible empty interface? require body. - return null; - } - - $len = strlen($text); - $depth = 0; - $in_str = false; - $str_q = ''; - $in_line_comment = false; - $in_block_comment = false; - $escape = false; - - for ($i = $brace; $i < $len; $i++) { - $ch = $text[$i]; - $next = $i + 1 < $len ? $text[$i + 1] : ''; - - if ($in_line_comment) { - if ($ch === "\n") { - $in_line_comment = false; - } - continue; - } - if ($in_block_comment) { - if ($ch === '*' && $next === '/') { - $in_block_comment = false; - $i++; - } - continue; - } - if ($in_str) { - if ($escape) { - $escape = false; - continue; - } - if ($ch === '\\') { - $escape = true; - continue; - } - if ($ch === $str_q) { - $in_str = false; - } - continue; - } - - if ($ch === '/' && $next === '/') { - $in_line_comment = true; - $i++; - continue; - } - if ($ch === '/' && $next === '*') { - $in_block_comment = true; - $i++; - continue; - } - if ($ch === '"' || $ch === "'") { - $in_str = true; - $str_q = $ch; - continue; - } - // Heredoc/nowdoc not handled fully — rare in this codebase. - - if ($ch === '{') { - $depth++; - } elseif ($ch === '}') { - $depth--; - if ($depth === 0) { - return substr($text, $start, $i - $start + 1); - } - } - } - return null; -} - -/** - * @param list> $entries - * @return array - */ -function aksrt_replacements(array $entries): array { - $rep = []; - foreach ($entries as $e) { - $rep[$e['old']] = '\\' . $e['new_fqcn']; - } - uksort($rep, static fn($a, $b) => strlen($b) <=> strlen($a)); - return $rep; -} - -/** - * @param array $rep - */ -function aksrt_rewrite_refs(string $code, array $rep): string { - $protected = []; - $i = 0; - foreach (PRESERVE_TOKENS as $tok) { - $ph = "\0PRESERVE{$i}\0"; - $protected[$ph] = $tok; - $code = str_replace($tok, $ph, $code); - $i++; - } - - if ($rep) { - $pattern = '/\b(' . implode('|', array_map(static fn($k) => preg_quote($k, '/'), array_keys($rep))) . ')\b/'; - $code = preg_replace_callback($pattern, static function ($m) use ($rep) { - return $rep[$m[1]] ?? $m[1]; - }, $code) ?? $code; - } - - foreach ($protected as $ph => $tok) { - $code = str_replace($ph, $tok, $code); - } - return $code; -} - -/** - * @param array $entry - * @param array $rep - */ -function aksrt_build_namespaced_file(array $entry, string $body, array $rep): string { - $kind = $entry['kind']; - $old = $entry['old']; - $short = $entry['short']; - $ns = $entry['namespace']; - - $body = preg_replace( - '/(abstract\s+|final\s+)?(' . preg_quote($kind, '/') . ')\s+' . preg_quote($old, '/') . '\b/', - '$1$2 ' . $short, - $body, - 1 - ) ?? $body; - - $body = aksrt_rewrite_refs($body, $rep); - - // Same-namespace FQCN → short name. - $body = preg_replace( - '/\\\\' . preg_quote($ns, '/') . '\\\\(\w+)/', - '$1', - $body - ) ?? $body; - - $out = " 'inc/modules/captcha.php', - 'inc/classes/payment/class-aksrt-creator-revenue.php' => 'inc/modules/creator-revenue.php', - 'inc/classes/user/class-aksrt-user-notifications.php' => 'inc/modules/user-notifications.php', - 'inc/classes/user/class-aksrt-user.php' => 'inc/modules/user-hub.php', - 'inc/classes/user/user-parts/aksrt-user-account-ajax.php' => 'inc/modules/user-parts/account-ajax.php', - 'inc/classes/user/user-parts/aksrt-user-routing.php' => 'inc/modules/user-parts/routing.php', - 'inc/classes/user/user-parts/aksrt-user-template-registry.php' => 'inc/modules/user-parts/template-registry.php', - 'inc/classes/user/user-parts/aksrt-user-urls.php' => 'inc/modules/user-parts/urls.php', - 'inc/classes/widgets/class-aksrt-widgets.php' => 'inc/modules/widgets-register.php', -]; - -foreach ($proc_moves as $from => $to) { - $from_abs = $root . '/' . $from; - $to_abs = $root . '/' . $to; - if (!is_file($from_abs) && is_file($to_abs)) { - continue; - } - if (!is_file($from_abs)) { - echo "warn: procedural missing {$from}\n"; - continue; - } - $text = (string) file_get_contents($from_abs); - if ($from === 'inc/classes/user/class-aksrt-user.php') { - $text = str_replace( - [ - "__DIR__ . '/user-parts/aksrt-user-template-registry.php'", - "__DIR__ . '/user-parts/aksrt-user-urls.php'", - "__DIR__ . '/user-parts/aksrt-user-routing.php'", - "__DIR__ . '/user-parts/aksrt-user-account-ajax.php'", - ], - [ - "__DIR__ . '/user-parts/template-registry.php'", - "__DIR__ . '/user-parts/urls.php'", - "__DIR__ . '/user-parts/routing.php'", - "__DIR__ . '/user-parts/account-ajax.php'", - ], - $text - ); - } - $text = aksrt_rewrite_refs($text, $rep); - aksrt_write($to_abs, $text, $dry); -} - -$scan_roots = [ - $root . '/inc/helpers', - $root . '/inc/core-hooks', - $root . '/inc/modules', - $root . '/templates', - $root . '/tools', -]; -$scan_files = [ - $root . '/functions.php', - $root . '/inc/bootstrap.php', - $root . '/inc/core-hooks.php', - $root . '/uninstall.php', - $root . '/header.php', - $root . '/footer.php', - $root . '/index.php', - $root . '/single.php', - $root . '/page.php', - $root . '/archive.php', - $root . '/search.php', - $root . '/category.php', - $root . '/tag.php', - $root . '/author.php', - $root . '/404.php', - $root . '/home.php', - $root . '/front-page.php', - $root . '/comments.php', - $root . '/sidebar.php', - $root . '/image.php', - $root . '/go.php', - $root . '/slim-forum.php', - $root . '/archive-resources.php', -]; - -$php_files = $scan_files; -foreach ($scan_roots as $dir) { - if (!is_dir($dir)) { - continue; - } - $rii = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); - foreach ($rii as $file) { - if ($file->isFile() && str_ends_with($file->getFilename(), '.php')) { - $php_files[] = $file->getPathname(); - } - } -} -$php_files = array_values(array_unique($php_files)); -$rewritten = 0; -foreach ($php_files as $abs) { - if (!is_file($abs)) { - continue; - } - $rel = str_replace('\\', '/', substr($abs, strlen($root) + 1)); - if (str_starts_with($rel, 'tools/build-psr4-map.php') - || str_starts_with($rel, 'tools/psr4-migrate.php') - || str_starts_with($rel, 'tools/psr4-map/')) { - continue; - } - if (str_starts_with($rel, 'src/')) { - continue; - } - - $orig = (string) file_get_contents($abs); - $next = aksrt_rewrite_refs($orig, $rep); - if ($next !== $orig) { - aksrt_write($abs, $next, $dry); - $rewritten++; - } -} -echo "Reference rewrites in non-src PHP: {$rewritten} files\n"; - -// Helper pure-class files become stubs after class move — rewrite domain-model/shortcodes if they still have class bodies (migrator already wrote src copies). -// After apply, delete old type sources and helper class-only file. -$helper_pure = [ - 'inc/helpers/class-aksrt-nav-walker.php', -]; - -if (!$dry) { - foreach (array_keys($old_type_paths) as $rel) { - $abs = $root . '/' . $rel; - if (is_file($abs)) { - unlink($abs); - } - } - foreach (array_keys($proc_moves) as $rel) { - $abs = $root . '/' . $rel; - if (is_file($abs)) { - unlink($abs); - } - } - foreach ($helper_pure as $rel) { - $abs = $root . '/' . $rel; - if (is_file($abs)) { - unlink($abs); - } - } - // helpers that still contain only moved classes: strip by rewriting shortcodes/domain-model to empty require-safe stubs if class was extracted. - foreach (['inc/helpers/aksrt-shortcodes.php', 'inc/helpers/aksrt-domain-model.php'] as $rel) { - $abs = $root . '/' . $rel; - if (!is_file($abs)) { - continue; - } - $t = (string) file_get_contents($abs); - // If file still has class keyword for types we moved, replace with comment stub. - if (preg_match('/\b(class|trait|interface)\s+/', $t)) { - file_put_contents( - $abs, - "isDir()) { - @rmdir($file->getPathname()); - } else { - echo 'leftover file: ' . $file->getPathname() . "\n"; - } - } - @rmdir($classes); - } -} else { - echo '[dry] would delete ' . count($old_type_paths) . " old type paths + procedural + prune inc/classes\n"; -} - -$lookup = []; -foreach ($entries as $e) { - $lookup[$e['old']] = $e['new_fqcn']; -} -$auth_fq = $lookup['AKSRT_Auth_Service'] ?? 'AKSRT\\Auth\\Service'; -$settings_fq = $lookup['AKSRT_Settings'] ?? 'AKSRT\\Settings\\Settings'; - -$bootstrap = << [ - 'AKSRT\\' => 'src/AKSRT/', - ], -]; -$composer['description'] = 'AKSRT WordPress Theme'; -$json = json_encode($composer, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . "\n"; -aksrt_write($composer_path, $json, $dry); - -// functions.php must require composer autoload (already does) — ensure fatal if missing for theme classes. -if (!$dry) { - $fn = (string) file_get_contents($root . '/functions.php'); - // Already rewritten class names. Optionally strengthen vendor require message. - if (!str_contains($fn, 'vendor/autoload.php')) { - fwrite(STDERR, "functions.php missing vendor/autoload.php require\n"); - } -} - -echo "Done.\n"; -exit(($failed && !$dry) ? 1 : 0);