GenerateAppTreeCommand.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <?php
  2. namespace UCore\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\File;
  5. /**
  6. * 生成 app 目录的文件树并保存到 app/tree.md
  7. */
  8. class GenerateAppTreeCommand extends Command
  9. {
  10. /**
  11. * 命令的名称和签名。
  12. *
  13. * @var string
  14. */
  15. protected $signature = 'ucore:generate-apptree';
  16. /**
  17. * 命令的描述。
  18. *
  19. * @var string
  20. */
  21. protected $description = '生成 app 目录的文件树并保存到 app/tree.md';
  22. /**
  23. * 统计数据
  24. *
  25. * @var array
  26. */
  27. protected array $statistics = [
  28. 'total_files' => 0,
  29. 'total_directories' => 0,
  30. 'php_files' => 0,
  31. 'non_php_files' => 0,
  32. 'classes_with_description' => 0,
  33. 'classes_without_description' => 0,
  34. 'file_types' => [],
  35. 'modules' => [],
  36. 'largest_files' => [],
  37. 'large_files_by_lines' => [], // 行数大于500的文件
  38. 'total_lines' => 0,
  39. 'average_lines_per_file' => 0,
  40. 'class_types' => [
  41. 'class' => 0,
  42. 'interface' => 0,
  43. 'trait' => 0,
  44. 'enum' => 0,
  45. ],
  46. ];
  47. /**
  48. * 执行控制台命令。
  49. *
  50. * @return int
  51. */
  52. public function handle()
  53. {
  54. $appPath = base_path('app');
  55. $outputFile = base_path('app/tree.md');
  56. // 初始化统计数据
  57. $this->resetStatistics();
  58. $tree = $this->generateDirectoryTree($appPath);
  59. // 添加统计信息
  60. $statistics = $this->generateStatistics();
  61. $content = $tree . "\n" . $statistics;
  62. File::put($outputFile, $content);
  63. $this->info('File tree generated successfully at app/tree.md');
  64. $this->displayStatistics();
  65. return Command::SUCCESS;
  66. }
  67. /**
  68. * 生成目录树结构为字符串
  69. *
  70. * @param string $directory 目录路径
  71. * @param string $prefix 前缀,用于格式化输出
  72. * @return string 返回目录树的字符串表示
  73. */
  74. private function generateDirectoryTree(string $directory, string $prefix = ''): string
  75. {
  76. $tree = '';
  77. $files = scandir($directory);
  78. foreach ($files as $file) {
  79. if ($file === '.' || $file === '..') {
  80. continue;
  81. }
  82. $filePath = $directory . DIRECTORY_SEPARATOR . $file;
  83. if (is_dir($filePath)) {
  84. $tree .= $prefix . $file . "\n";
  85. $this->updateDirectoryStatistics();
  86. $tree .= $this->generateDirectoryTree($filePath, $prefix . ' ');
  87. } else {
  88. // 对于文件,添加类信息
  89. $fileInfo = $this->getFileInfo($filePath, $file);
  90. $this->updateFileStatistics($filePath, $file, $fileInfo);
  91. $tree .= $prefix . $fileInfo . "\n";
  92. }
  93. }
  94. return $tree;
  95. }
  96. /**
  97. * 获取文件信息(文件名 + 类名和简述)
  98. *
  99. * @param string $filePath 文件完整路径
  100. * @param string $fileName 文件名
  101. * @return string 格式化的文件信息
  102. */
  103. private function getFileInfo(string $filePath, string $fileName): string
  104. {
  105. // 只处理 PHP 文件
  106. if (!str_ends_with($fileName, '.php')) {
  107. return $fileName;
  108. }
  109. $classInfo = $this->extractClassInfo($filePath);
  110. if ($classInfo) {
  111. return $fileName . ' - ' . $classInfo;
  112. }
  113. return $fileName;
  114. }
  115. /**
  116. * 从 PHP 文件中提取类名和简述
  117. *
  118. * @param string $filePath 文件路径
  119. * @return string|null 类名和简述,格式:ClassName: 类简述
  120. */
  121. private function extractClassInfo(string $filePath): ?string
  122. {
  123. try {
  124. $content = File::get($filePath);
  125. // 提取类名
  126. $className = $this->extractClassName($content);
  127. if (!$className) {
  128. return null;
  129. }
  130. // 提取类注释中的简述
  131. $description = $this->extractClassDescription($content);
  132. if ($description) {
  133. return $className . ': ' . $description;
  134. }
  135. return $className;
  136. } catch (\Exception $e) {
  137. return null;
  138. }
  139. }
  140. /**
  141. * 提取类名
  142. *
  143. * @param string $content 文件内容
  144. * @return string|null 类名
  145. */
  146. private function extractClassName(string $content): ?string
  147. {
  148. // 匹配 class、interface、trait、enum 声明
  149. $patterns = [
  150. '/class\s+([a-zA-Z_][a-zA-Z0-9_]*)/i',
  151. '/interface\s+([a-zA-Z_][a-zA-Z0-9_]*)/i',
  152. '/trait\s+([a-zA-Z_][a-zA-Z0-9_]*)/i',
  153. '/enum\s+([a-zA-Z_][a-zA-Z0-9_]*)/i',
  154. ];
  155. foreach ($patterns as $pattern) {
  156. if (preg_match($pattern, $content, $matches)) {
  157. return $matches[1];
  158. }
  159. }
  160. return null;
  161. }
  162. /**
  163. * 提取类注释中的简述
  164. *
  165. * @param string $content 文件内容
  166. * @return string|null 类简述
  167. */
  168. private function extractClassDescription(string $content): ?string
  169. {
  170. // 匹配类声明前的文档注释 - 改进的正则表达式
  171. $pattern = '/\/\*\*\s*(.*?)\s*\*\/\s*(?:abstract\s+|final\s+)?(?:class|interface|trait|enum)\s+[a-zA-Z_][a-zA-Z0-9_]*/s';
  172. if (preg_match($pattern, $content, $matches)) {
  173. $docComment = $matches[1];
  174. // 清理注释格式,提取第一行有意义的描述
  175. $lines = explode("\n", $docComment);
  176. foreach ($lines as $line) {
  177. $line = trim($line);
  178. $line = preg_replace('/^\*\s*/', '', $line); // 移除行首的 *
  179. $line = trim($line);
  180. // 跳过空行和 @标签
  181. if (empty($line) || str_starts_with($line, '@')) {
  182. continue;
  183. }
  184. // 返回第一行有意义的描述
  185. return $line;
  186. }
  187. }
  188. // 如果上面的模式没有匹配,尝试更宽松的模式
  189. $pattern2 = '/\/\*\*\s*\n\s*\*\s*([^\n\*@]+)/';
  190. if (preg_match($pattern2, $content, $matches)) {
  191. return trim($matches[1]);
  192. }
  193. return null;
  194. }
  195. /**
  196. * 重置统计数据
  197. *
  198. * @return void
  199. */
  200. private function resetStatistics(): void
  201. {
  202. $this->statistics = [
  203. 'total_files' => 0,
  204. 'total_directories' => 0,
  205. 'php_files' => 0,
  206. 'non_php_files' => 0,
  207. 'classes_with_description' => 0,
  208. 'classes_without_description' => 0,
  209. 'file_types' => [],
  210. 'modules' => [],
  211. 'largest_files' => [],
  212. 'large_files_by_lines' => [],
  213. 'total_lines' => 0,
  214. 'average_lines_per_file' => 0,
  215. 'class_types' => [
  216. 'class' => 0,
  217. 'interface' => 0,
  218. 'trait' => 0,
  219. 'enum' => 0,
  220. ],
  221. ];
  222. }
  223. /**
  224. * 更新文件统计信息
  225. *
  226. * @param string $filePath 文件路径
  227. * @param string $fileName 文件名
  228. * @param string|null $classInfo 类信息
  229. * @return void
  230. */
  231. private function updateFileStatistics(string $filePath, string $fileName, ?string $classInfo): void
  232. {
  233. $this->statistics['total_files']++;
  234. // 统计文件类型
  235. $extension = strtolower(pathinfo($fileName, PATHINFO_EXTENSION));
  236. if (!isset($this->statistics['file_types'][$extension])) {
  237. $this->statistics['file_types'][$extension] = 0;
  238. }
  239. $this->statistics['file_types'][$extension]++;
  240. // 统计 PHP 文件
  241. if ($extension === 'php') {
  242. $this->statistics['php_files']++;
  243. // 统计类描述
  244. if ($classInfo && strpos($classInfo, ':') !== false) {
  245. $this->statistics['classes_with_description']++;
  246. } else {
  247. $this->statistics['classes_without_description']++;
  248. }
  249. // 统计类类型
  250. $this->updateClassTypeStatistics($filePath);
  251. } else {
  252. $this->statistics['non_php_files']++;
  253. }
  254. // 统计模块
  255. $this->updateModuleStatistics($filePath);
  256. // 统计大文件
  257. $this->updateLargestFiles($filePath, $fileName);
  258. // 统计文件行数
  259. $this->updateLineStatistics($filePath, $fileName);
  260. }
  261. /**
  262. * 更新目录统计信息
  263. *
  264. * @return void
  265. */
  266. private function updateDirectoryStatistics(): void
  267. {
  268. $this->statistics['total_directories']++;
  269. }
  270. /**
  271. * 更新类类型统计
  272. *
  273. * @param string $filePath 文件路径
  274. * @return void
  275. */
  276. private function updateClassTypeStatistics(string $filePath): void
  277. {
  278. try {
  279. $content = File::get($filePath);
  280. if (preg_match('/\bclass\s+[a-zA-Z_][a-zA-Z0-9_]*/', $content)) {
  281. $this->statistics['class_types']['class']++;
  282. } elseif (preg_match('/\binterface\s+[a-zA-Z_][a-zA-Z0-9_]*/', $content)) {
  283. $this->statistics['class_types']['interface']++;
  284. } elseif (preg_match('/\btrait\s+[a-zA-Z_][a-zA-Z0-9_]*/', $content)) {
  285. $this->statistics['class_types']['trait']++;
  286. } elseif (preg_match('/\benum\s+[a-zA-Z_][a-zA-Z0-9_]*/', $content)) {
  287. $this->statistics['class_types']['enum']++;
  288. }
  289. } catch (\Exception $e) {
  290. // 忽略文件读取错误
  291. }
  292. }
  293. /**
  294. * 更新模块统计
  295. *
  296. * @param string $filePath 文件路径
  297. * @return void
  298. */
  299. private function updateModuleStatistics(string $filePath): void
  300. {
  301. // 检查是否在 Module 目录下
  302. if (strpos($filePath, '/Module/') !== false) {
  303. $parts = explode('/Module/', $filePath);
  304. if (count($parts) > 1) {
  305. $moduleParts = explode('/', $parts[1]);
  306. $moduleName = $moduleParts[0];
  307. if (!isset($this->statistics['modules'][$moduleName])) {
  308. $this->statistics['modules'][$moduleName] = 0;
  309. }
  310. $this->statistics['modules'][$moduleName]++;
  311. }
  312. }
  313. }
  314. /**
  315. * 更新最大文件统计
  316. *
  317. * @param string $filePath 文件路径
  318. * @param string $fileName 文件名
  319. * @return void
  320. */
  321. private function updateLargestFiles(string $filePath, string $fileName): void
  322. {
  323. try {
  324. $fileSize = filesize($filePath);
  325. $this->statistics['largest_files'][] = [
  326. 'name' => $fileName,
  327. 'size' => $fileSize,
  328. 'path' => $filePath
  329. ];
  330. // 只保留前10个最大的文件
  331. usort($this->statistics['largest_files'], function($a, $b) {
  332. return $b['size'] - $a['size'];
  333. });
  334. if (count($this->statistics['largest_files']) > 10) {
  335. $this->statistics['largest_files'] = array_slice($this->statistics['largest_files'], 0, 10);
  336. }
  337. } catch (\Exception $e) {
  338. // 忽略文件大小获取错误
  339. }
  340. }
  341. /**
  342. * 更新文件行数统计
  343. *
  344. * @param string $filePath 文件路径
  345. * @param string $fileName 文件名
  346. * @return void
  347. */
  348. private function updateLineStatistics(string $filePath, string $fileName): void
  349. {
  350. try {
  351. $content = File::get($filePath);
  352. $lineCount = substr_count($content, "\n") + 1;
  353. // 累计总行数
  354. $this->statistics['total_lines'] += $lineCount;
  355. // 如果行数大于500,添加到大文件列表
  356. if ($lineCount > 500) {
  357. $this->statistics['large_files_by_lines'][] = [
  358. 'name' => $fileName,
  359. 'lines' => $lineCount,
  360. 'path' => $filePath,
  361. 'size' => filesize($filePath)
  362. ];
  363. // 按行数排序,只保留前20个
  364. usort($this->statistics['large_files_by_lines'], function($a, $b) {
  365. return $b['lines'] - $a['lines'];
  366. });
  367. if (count($this->statistics['large_files_by_lines']) > 20) {
  368. $this->statistics['large_files_by_lines'] = array_slice($this->statistics['large_files_by_lines'], 0, 20);
  369. }
  370. }
  371. } catch (\Exception $e) {
  372. // 忽略文件读取错误
  373. }
  374. }
  375. /**
  376. * 生成统计信息
  377. *
  378. * @return string
  379. */
  380. private function generateStatistics(): string
  381. {
  382. $stats = $this->statistics;
  383. $output = "\n" . str_repeat("=", 80) . "\n";
  384. $output .= "📊 项目统计信息\n";
  385. $output .= str_repeat("=", 80) . "\n\n";
  386. // 基础统计
  387. $output .= "📁 **文件和目录统计**\n";
  388. $output .= "- 总文件数: {$stats['total_files']}\n";
  389. $output .= "- 总目录数: {$stats['total_directories']}\n";
  390. $output .= "- PHP 文件: {$stats['php_files']}\n";
  391. $output .= "- 非 PHP 文件: {$stats['non_php_files']}\n\n";
  392. // 行数统计
  393. $averageLines = $stats['total_files'] > 0 ? round($stats['total_lines'] / $stats['total_files'], 1) : 0;
  394. $largeFilesCount = count($stats['large_files_by_lines']);
  395. $output .= "📏 **代码行数统计**\n";
  396. $output .= "- 总代码行数: " . number_format($stats['total_lines']) . "\n";
  397. $output .= "- 平均每文件行数: {$averageLines}\n";
  398. $output .= "- 大文件数量 (>500行): {$largeFilesCount}\n\n";
  399. // 类描述统计
  400. $totalClasses = $stats['classes_with_description'] + $stats['classes_without_description'];
  401. $descriptionPercentage = $totalClasses > 0 ? round(($stats['classes_with_description'] / $totalClasses) * 100, 1) : 0;
  402. $output .= "📝 **类注释统计**\n";
  403. $output .= "- 有描述的类: {$stats['classes_with_description']}\n";
  404. $output .= "- 无描述的类: {$stats['classes_without_description']}\n";
  405. $output .= "- 注释覆盖率: {$descriptionPercentage}%\n\n";
  406. // 类类型统计
  407. $output .= "🏗️ **类类型统计**\n";
  408. foreach ($stats['class_types'] as $type => $count) {
  409. if ($count > 0) {
  410. $output .= "- " . ucfirst($type) . ": {$count}\n";
  411. }
  412. }
  413. $output .= "\n";
  414. // 文件类型统计
  415. if (!empty($stats['file_types'])) {
  416. $output .= "📄 **文件类型统计**\n";
  417. arsort($stats['file_types']);
  418. foreach ($stats['file_types'] as $ext => $count) {
  419. $ext = $ext ?: '(无扩展名)';
  420. $output .= "- .{$ext}: {$count}\n";
  421. }
  422. $output .= "\n";
  423. }
  424. // 模块统计
  425. if (!empty($stats['modules'])) {
  426. $output .= "📦 **模块统计**\n";
  427. arsort($stats['modules']);
  428. foreach ($stats['modules'] as $module => $count) {
  429. $output .= "- {$module}: {$count} 个文件\n";
  430. }
  431. $output .= "\n";
  432. }
  433. // 最大文件统计(按文件大小)
  434. if (!empty($stats['largest_files'])) {
  435. $output .= "💾 **最大文件 - 按文件大小 (Top 5)**\n";
  436. $topFiles = array_slice($stats['largest_files'], 0, 5);
  437. foreach ($topFiles as $file) {
  438. $size = $this->formatFileSize($file['size']);
  439. $relativePath = str_replace(base_path('app') . '/', '', $file['path']);
  440. $output .= "- {$file['name']}: {$size} ({$relativePath})\n";
  441. }
  442. $output .= "\n";
  443. }
  444. // 大文件统计(按行数)
  445. if (!empty($stats['large_files_by_lines'])) {
  446. $output .= "📏 **大文件 - 按代码行数 (>500行, Top 10)**\n";
  447. $topFiles = array_slice($stats['large_files_by_lines'], 0, 10);
  448. foreach ($topFiles as $file) {
  449. $size = $this->formatFileSize($file['size']);
  450. $relativePath = str_replace(base_path('app') . '/', '', $file['path']);
  451. $output .= "- {$file['name']}: {$file['lines']} 行 ({$size}) - {$relativePath}\n";
  452. }
  453. $output .= "\n";
  454. }
  455. // 生成时间
  456. $output .= "⏰ **生成信息**\n";
  457. $output .= "- 生成时间: " . date('Y-m-d H:i:s') . "\n";
  458. $output .= "- 生成命令: php artisan ucore:generate-apptree\n";
  459. $output .= "- UCore 版本: 1.0\n\n";
  460. $output .= str_repeat("=", 80) . "\n";
  461. $output .= "🎉 文件树生成完成!\n";
  462. $output .= str_repeat("=", 80);
  463. return $output;
  464. }
  465. /**
  466. * 在控制台显示统计信息
  467. *
  468. * @return void
  469. */
  470. private function displayStatistics(): void
  471. {
  472. $stats = $this->statistics;
  473. $this->newLine();
  474. $this->info('📊 统计信息:');
  475. $this->line("文件总数: {$stats['total_files']}");
  476. $this->line("目录总数: {$stats['total_directories']}");
  477. $this->line("PHP 文件: {$stats['php_files']}");
  478. // 行数统计
  479. $averageLines = $stats['total_files'] > 0 ? round($stats['total_lines'] / $stats['total_files'], 1) : 0;
  480. $largeFilesCount = count($stats['large_files_by_lines']);
  481. $this->line("总代码行数: " . number_format($stats['total_lines']));
  482. $this->line("平均每文件行数: {$averageLines}");
  483. $this->line("大文件数量 (>500行): {$largeFilesCount}");
  484. $totalClasses = $stats['classes_with_description'] + $stats['classes_without_description'];
  485. $descriptionPercentage = $totalClasses > 0 ? round(($stats['classes_with_description'] / $totalClasses) * 100, 1) : 0;
  486. $this->line("类注释覆盖率: {$descriptionPercentage}%");
  487. if (!empty($stats['modules'])) {
  488. $moduleCount = count($stats['modules']);
  489. $this->line("模块数量: {$moduleCount}");
  490. }
  491. }
  492. /**
  493. * 格式化文件大小
  494. *
  495. * @param int $bytes
  496. * @return string
  497. */
  498. private function formatFileSize(int $bytes): string
  499. {
  500. $units = ['B', 'KB', 'MB', 'GB'];
  501. $bytes = max($bytes, 0);
  502. $pow = floor(($bytes ? log($bytes) : 0) / log(1024));
  503. $pow = min($pow, count($units) - 1);
  504. $bytes /= pow(1024, $pow);
  505. return round($bytes, 2) . ' ' . $units[$pow];
  506. }
  507. }