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.
374 lines
9.9 KiB
PHP
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);
|