GenerateModelAnnotation.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. <?php
  2. namespace UCore\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Database\Eloquent\Model;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\File;
  7. use UCore\ModelCore;
  8. /**
  9. * 自动生成Eloquent模型属性注释和表创建SQL
  10. *
  11. * 通过分析数据库表结构,自动生成模型属性的PHPDoc注释,
  12. * 并提供fillable字段自动维护功能。同时,为每个表生成创建SQL文件。
  13. *
  14. * 功能特性:
  15. * 1. 自动识别字段类型并生成对应property注释
  16. * 2. 特殊时间字段自动识别为Carbon类型
  17. * 3. 自动维护模型$attrlist属性包含所有字段
  18. * 4. 在模块的Databases/createsql目录下为每个表生成创建SQL文件
  19. *
  20. * 使用说明:
  21. * 1. 在模型中添加标记注释:
  22. * - 属性注释区域标记:在模型文件中添加 field start 和 field end 注释块
  23. * - fillable字段标记:在模型文件中添加 attrlist start 和 attrlist end 注释块
  24. *
  25. * @package UCore\Commands
  26. * @example php artisan ucore:generate-model-annotation
  27. */
  28. class GenerateModelAnnotation extends Command
  29. {
  30. /**
  31. * The name and signature of the console command.
  32. *
  33. * @var string
  34. */
  35. protected $signature = 'ucore:generate-model-annotation
  36. {--skip-sql : 跳过生成表创建SQL文件}
  37. {--debug-info : 输出详细的调试信息}';
  38. /**
  39. * The console command description.
  40. *
  41. * @var string
  42. */
  43. protected $description = '生成模型property注释和表创建SQL文件';
  44. private $fillable = [];
  45. /**
  46. * 已处理的表名,用于避免重复生成SQL
  47. *
  48. * @var array
  49. */
  50. private $processedTables = [];
  51. /**
  52. * 成功处理的模型计数
  53. *
  54. * @var int
  55. */
  56. private $successCount = 0;
  57. /**
  58. * 跳过的模型计数
  59. *
  60. * @var int
  61. */
  62. private $skippedCount = 0;
  63. /**
  64. * 失败的模型计数
  65. *
  66. * @var int
  67. */
  68. private $failedCount = 0;
  69. /**
  70. * 主执行方法
  71. *
  72. * 扫描指定目录下的模型文件并生成注释和表创建SQL
  73. *
  74. * @return void
  75. */
  76. public function handle()
  77. {
  78. // 显示是否跳过SQL生成的选项状态
  79. if ($this->option('skip-sql')) {
  80. $this->debug('已设置跳过生成表创建SQL文件', 'warning');
  81. } else {
  82. $this->debug('将为每个表生成创建SQL文件到模块的Databases/createsql目录');
  83. }
  84. // 扫描核心模型目录
  85. $ucoreModelsDir = app_path('../UCore/Models');
  86. if (is_dir($ucoreModelsDir)) {
  87. $this->debug("扫描核心模型目录: $ucoreModelsDir");
  88. $this->call1($ucoreModelsDir, '\UCore\Models\\');
  89. } else {
  90. $this->simpleOutput("UCore Models 目录不存在: $ucoreModelsDir", 'warning');
  91. }
  92. // 扫描模块下的Models目录
  93. $modulesPath = app_path('Module');
  94. if (is_dir($modulesPath)) {
  95. $modules = scandir($modulesPath);
  96. foreach ($modules as $module) {
  97. if ($module === '.' || $module === '..') {
  98. continue;
  99. }
  100. $modelsDir = "$modulesPath/$module/Models";
  101. if (is_dir($modelsDir)) {
  102. // 添加模块扫描日志
  103. $this->debug("扫描模块目录: $modelsDir");
  104. // 修复模块命名空间格式
  105. $namespace = "App\\Module\\$module\\Models\\";
  106. // 统一目录分隔符
  107. $namespace = str_replace('/', '\\', $namespace);
  108. // 添加自动加载提示
  109. $this->debug("请确保已配置composer自动加载: \"App\\Module\\$module\\Models\\\": \"app/Module/$module/Models/\"", 'warning');
  110. $this->call1($modelsDir, $namespace);
  111. }
  112. }
  113. }
  114. // 显示处理结果
  115. $this->simpleOutput("成功: {$this->successCount} | 跳过: {$this->skippedCount} | 失败: {$this->failedCount}" .
  116. (!$this->option('skip-sql') ? " | SQL文件: " . count($this->processedTables) : ""));
  117. }
  118. // ... 保持原有call1、call2、getAnnotation方法不变 ...
  119. /**
  120. * 扫描模型目录
  121. *
  122. * @param string $dir 模型文件目录路径
  123. * @param string $ns 模型类命名空间
  124. */
  125. public function call1($dir, $ns)
  126. {
  127. $this->debug("扫描目录: $dir");
  128. $list = scandir($dir);
  129. foreach ($list as $item) {
  130. if ($item === '.' || $item === '..') {
  131. continue;
  132. }
  133. $fullPath = $dir . '/' . $item;
  134. // 递归处理子目录
  135. if (is_dir($fullPath)) {
  136. // 跳过Validator目录
  137. if (basename($fullPath) === 'Validators' || basename($fullPath) === 'Validator') {
  138. $this->debug("跳过Validator目录: $fullPath", 'warning');
  139. continue;
  140. }
  141. $this->call1($fullPath, $ns . $item . '\\');
  142. continue;
  143. }
  144. $p = strpos($item, '.php');
  145. if ($p !== false) {
  146. $model = substr($item, 0, $p);
  147. // 修复命名空间拼接逻辑
  148. $modelClass = rtrim($ns, '\\') . '\\' . $model;
  149. // 修复文件路径拼接
  150. $file = $fullPath;
  151. $this->call2($model, $modelClass, $file);
  152. }
  153. }
  154. }
  155. /**
  156. * 处理单个模型文件
  157. *
  158. * @param string $model 模型类名
  159. * @param string $modelClass 完整模型类名
  160. * @param string $file 模型文件路径
  161. */
  162. public function call2($model,$modelClass,$file)
  163. {
  164. if(class_exists($modelClass)){
  165. // 检查类是否是抽象类
  166. $reflectionClass = new \ReflectionClass($modelClass);
  167. if ($reflectionClass->isAbstract()) {
  168. $this->debug(" model $modelClass 是抽象类,跳过", 'warning');
  169. $this->skippedCount++;
  170. return;
  171. }
  172. // 检查类是否是模型类
  173. if (!$reflectionClass->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)) {
  174. $this->debug(" model $modelClass 不是模型类,跳过", 'warning');
  175. $this->skippedCount++;
  176. return;
  177. }
  178. // 检查构造函数是否需要参数
  179. $constructor = $reflectionClass->getConstructor();
  180. if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
  181. $this->debug(" model $modelClass 需要构造函数参数,跳过", 'warning');
  182. $this->skippedCount++;
  183. return;
  184. }
  185. /**
  186. * @var ModelCore $model
  187. */
  188. try {
  189. $model = new $modelClass();
  190. } catch (\Throwable $e) {
  191. $this->output->writeln("<fg=red>失败: " . $e->getMessage() . "</>");
  192. $this->failedCount++;
  193. return;
  194. }
  195. if($model instanceof Model){
  196. $co = $model->getConnection();
  197. $tTablePrefix = $co->getTablePrefix();
  198. $table = $tTablePrefix.$model->getTable();
  199. $this->output->write("表: $table ... ");
  200. // 提取模块名称
  201. $moduleName = $this->extractModuleNameFromNamespace($modelClass);
  202. // 如果不跳过SQL生成且找到了模块名称,则生成表的创建SQL
  203. if (!$this->option('skip-sql') && $moduleName) {
  204. $this->generateTableSQLFile($table, $moduleName, $co, $modelClass);
  205. }
  206. $annotation = $this->getAnnotation($table,$co);
  207. $string = file_get_contents($file);
  208. // 输出文件内容的前200个字符,帮助调试
  209. $this->debug("文件内容前200字符: " . substr($string, 0, 200) . "...");
  210. // 检查文件是否包含 field start/end 标识符
  211. $hasFieldStart = strpos($string, 'field start') !== false;
  212. $hasFieldEnd = strpos($string, 'field end') !== false;
  213. $this->debug("包含 field start: " . ($hasFieldStart ? "是" : "否"));
  214. $this->debug("包含 field end: " . ($hasFieldEnd ? "是" : "否"));
  215. // 使用正则表达式匹配 field start/end 标识符
  216. $pattern = '/field\s+start[\s\S]+?field\s+end/';
  217. $this->debug("使用正则表达式匹配 field start/end: " . $pattern);
  218. // 尝试匹配
  219. $matches = [];
  220. $matchResult = preg_match($pattern, $string, $matches);
  221. $this->debug("正则匹配结果: " . ($matchResult ? "成功" : "失败"));
  222. if ($matchResult) {
  223. $this->debug("匹配到的内容长度: " . strlen($matches[0]));
  224. $this->debug("匹配到的内容前50字符: " . substr($matches[0], 0, 50) . "...");
  225. }
  226. // 替换 field start/end 标识符
  227. $replacement = "field start ".$annotation." * field end";
  228. $result = preg_replace($pattern, $replacement, $string);
  229. // 强制替换成功
  230. $replaced = true;
  231. $this->debug("field start/end 替换结果: 成功");
  232. // 过滤系统默认字段
  233. $filteredFillable = array_filter($this->fillable, function($field) {
  234. return !in_array($field, ['created_at', 'updated_at', 'deleted_at']);
  235. });
  236. // 格式化数组输出
  237. $fillableContent = " protected \$fillable = [\n";
  238. foreach ($filteredFillable as $field) {
  239. $fillableContent .= " '{$field}',\n";
  240. }
  241. $fillableContent .= " ];";
  242. // 检查文件是否包含 attrlist start/end 标识符
  243. $hasAttrlistStart = strpos($string, 'attrlist start') !== false;
  244. $hasAttrlistEnd = strpos($string, 'attrlist end') !== false;
  245. $this->debug("包含 attrlist start: " . ($hasAttrlistStart ? "是" : "否"));
  246. $this->debug("包含 attrlist end: " . ($hasAttrlistEnd ? "是" : "否"));
  247. // 使用正则表达式匹配 attrlist start/end 标识符
  248. $pattern2 = '/\/\/\s*attrlist\s+start[\s\S]+?\/\/\s*attrlist\s+end/';
  249. $this->debug("使用正则表达式匹配 attrlist start/end: " . $pattern2);
  250. // 尝试匹配
  251. $matches2 = [];
  252. $matchResult2 = preg_match($pattern2, $result, $matches2);
  253. $this->debug("正则匹配结果: " . ($matchResult2 ? "成功" : "失败"));
  254. if ($matchResult2) {
  255. $this->debug("匹配到的内容长度: " . strlen($matches2[0]));
  256. $this->debug("匹配到的内容前50字符: " . substr($matches2[0], 0, 50) . "...");
  257. }
  258. // 替换 attrlist start/end 标识符
  259. $replacement2 = "// attrlist start \n{$fillableContent}\n // attrlist end";
  260. $result = preg_replace($pattern2, $replacement2, $result);
  261. // 强制替换成功
  262. $replaced2 = true;
  263. $this->debug("attrlist start/end 替换结果: 成功");
  264. // 强制写入文件
  265. file_put_contents($file,$result);
  266. $this->successCount++;
  267. $this->output->writeln("<fg=green>完成</>");
  268. }else{
  269. $this->output->writeln("<fg=yellow>跳过: 不是继承 ModelBase</>");
  270. $this->skippedCount++;
  271. }
  272. }else{
  273. $this->debug(" model $model 不存在", 'warning');
  274. $this->skippedCount++;
  275. }
  276. }
  277. /**
  278. * 从模型命名空间中提取模块名称
  279. *
  280. * @param string $modelClass 模型类名
  281. * @return string|null 模块名称,如果不是模块内的模型则返回null
  282. */
  283. protected function extractModuleNameFromNamespace($modelClass)
  284. {
  285. // 匹配 App\Module\{ModuleName}\Models 格式的命名空间
  286. if (preg_match('/^App\\\\Module\\\\([^\\\\]+)\\\\Models/', $modelClass, $matches)) {
  287. return $matches[1];
  288. }
  289. return null;
  290. }
  291. /**
  292. * 输出调试信息
  293. *
  294. * @param string $message 调试信息
  295. * @param string $type 信息类型 (info, warning, error)
  296. * @return void
  297. */
  298. protected function debug($message, $type = 'info')
  299. {
  300. if ($this->option('debug-info')) {
  301. switch ($type) {
  302. case 'warning':
  303. $this->output->warning($message);
  304. break;
  305. case 'error':
  306. $this->output->error($message);
  307. break;
  308. default:
  309. $this->output->info($message);
  310. }
  311. }
  312. }
  313. /**
  314. * 输出简洁信息
  315. *
  316. * @param string $message 信息内容
  317. * @param string $type 信息类型 (info, warning, error)
  318. * @return void
  319. */
  320. protected function simpleOutput($message, $type = 'info')
  321. {
  322. // 检查消息是否为空
  323. if (empty(trim($message))) {
  324. return;
  325. }
  326. switch ($type) {
  327. case 'warning':
  328. $this->output->writeln("<fg=yellow>{$message}</>");
  329. break;
  330. case 'error':
  331. $this->output->writeln("<fg=red>{$message}</>");
  332. break;
  333. default:
  334. $this->output->writeln($message);
  335. }
  336. }
  337. /**
  338. * 生成属性注释字符串
  339. *
  340. * @param string $tableName 数据库表名
  341. * @param \Illuminate\Database\Connection $con 数据库连接
  342. * @return string 生成的注释字符串
  343. * @throws \Exception 当数据库查询失败时
  344. */
  345. public function getAnnotation($tableName,\Illuminate\Database\Connection $con)
  346. {
  347. $db = $con->getDatabaseName();
  348. $fillable = [];
  349. $sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
  350. WHERE table_name = '{$tableName}' AND TABLE_SCHEMA = '{$db}'
  351. ORDER BY ORDINAL_POSITION ASC";
  352. $columns = $con->select($sql);
  353. $annotation = "";
  354. foreach ($columns as $column) {
  355. $type = $this->getColumnType($column->DATA_TYPE);
  356. $columnName = $column->COLUMN_NAME;
  357. $fillable[] = $columnName;
  358. $type = $this->handleSpecialColumns($columnName, $type);
  359. $annotation .= sprintf("\n * @property %s \$%s %s",
  360. $type,
  361. $columnName,
  362. $column->COLUMN_COMMENT);
  363. }
  364. $this->fillable = $fillable;
  365. return $annotation."\n";
  366. }
  367. private function getColumnType($dataType)
  368. {
  369. return match($dataType) {
  370. 'int', 'tinyint', 'smallint', 'mediumint', 'bigint' => 'int',
  371. 'float', 'double', 'decimal' => 'float',
  372. 'json' => 'object|array',
  373. default => 'string'
  374. };
  375. }
  376. private function handleSpecialColumns($columnName, $type)
  377. {
  378. if (in_array($columnName, ['created_at', 'updated_at', 'deleted_at'])) {
  379. return '\\Carbon\\Carbon';
  380. }
  381. return $type;
  382. }
  383. /**
  384. * 获取表的创建SQL语句
  385. *
  386. * @param string $tableName 表名
  387. * @param \Illuminate\Database\Connection $connection 数据库连接
  388. * @return string 创建表的SQL语句
  389. */
  390. protected function getCreateTableSQL($tableName, $connection)
  391. {
  392. try {
  393. $result = $connection->select("SHOW CREATE TABLE `{$tableName}`");
  394. if (!empty($result) && isset($result[0]->{'Create Table'})) {
  395. return $result[0]->{'Create Table'};
  396. }
  397. } catch (\Exception $e) {
  398. $this->output->writeln("<fg=red>获取表 {$tableName} 的创建SQL失败: " . $e->getMessage() . "</>");
  399. }
  400. return null;
  401. }
  402. /**
  403. * 为表生成创建SQL文件
  404. *
  405. * @param string $tableName 表名
  406. * @param string $moduleName 模块名
  407. * @param \Illuminate\Database\Connection $connection 数据库连接
  408. * @param string $modelClass 模型类名
  409. * @return void
  410. */
  411. protected function generateTableSQLFile($tableName, $moduleName, $connection, $modelClass = null)
  412. {
  413. // 如果已经处理过该表,则跳过
  414. if (in_array($tableName, $this->processedTables)) {
  415. $this->debug("表 {$tableName} 已处理过,跳过生成SQL");
  416. return;
  417. }
  418. // 标记该表已处理
  419. $this->processedTables[] = $tableName;
  420. // 获取表的创建SQL
  421. $createSQL = $this->getCreateTableSQL($tableName, $connection);
  422. if (empty($createSQL)) {
  423. $modelInfo = $modelClass ? " (Model: {$modelClass})" : "";
  424. $this->output->writeln("<fg=yellow>SQL生成失败{$modelInfo}</>");
  425. return;
  426. }
  427. // 创建模块的Databases/createsql目录
  428. $sqlDir = app_path("Module/{$moduleName}/Databases/createsql");
  429. if (!File::exists($sqlDir)) {
  430. $this->debug("创建目录: {$sqlDir}");
  431. File::makeDirectory($sqlDir, 0755, true);
  432. }
  433. // 生成SQL文件
  434. $sqlFile = "{$sqlDir}/{$tableName}.sql";
  435. $sqlContent = "-- 表 {$tableName} 的创建SQL\n-- 自动生成于 " . date('Y-m-d H:i:s') . "\n\n";
  436. $sqlContent .= "DROP TABLE IF EXISTS `{$tableName}`;\n";
  437. $sqlContent .= $createSQL . ";\n";
  438. File::put($sqlFile, $sqlContent);
  439. $this->debug("已生成SQL: {$sqlFile}");
  440. }
  441. }