Files
wordpress-theme-aksrt/tools/build-psr4-map.php
T
kate522 aaf458af1c refactor(autoload): migrate theme classes to Composer PSR-4
Move 288 types from global AKSRT_* + inc/classes manual requires into
namespace AKSRT\{Domain} under src/AKSRT/, wire composer.json psr-4,
and slim bootstrap to helpers/modules/hooks only. Split multi-class
widget/gateway files, update admin contract audit paths, and document
the layout in ADR/README.
2026-07-17 13:29:22 +08:00

374 lines
9.9 KiB
PHP

#!/usr/bin/env php
<?php
/**
* Build PSR-4 class map for AKSRT theme classes.
*
* Usage: php tools/build-psr4-map.php [--write]
* Writes tools/psr4-map/map.json when --write is passed.
*
* @package AKSRT
*/
declare(strict_types=1);
$root = dirname(__DIR__);
$write = in_array('--write', $argv, true);
/**
* Domain folder (lowercase) → namespace segment.
*
* @var array<string, string>
*/
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<string, string>
*/
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<string, string>
*/
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<string>
*/
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<array{kind:string,name:string,path:string}>
*/
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<array{kind:string,name:?string,path:string}> $scanned
* @return array{entries: list<array<string,mixed>>, procedural: list<string>, errors: list<string>}
*/
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);