GenerateModelAnnotation.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307
  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 UCore\ModelCore;
  7. /**
  8. * 自动生成Eloquent模型属性注释
  9. *
  10. * 通过分析数据库表结构,自动生成模型属性的PHPDoc注释,
  11. * 并提供fillable字段自动维护功能。
  12. *
  13. * 功能特性:
  14. * 1. 自动识别字段类型并生成对应property注释
  15. * 2. 特殊时间字段自动识别为Carbon类型
  16. * 3. 自动维护模型$attrlist属性包含所有字段
  17. *
  18. * 使用说明:
  19. * 1. 在模型中添加标记注释:
  20. * - 属性注释区域标记:在模型文件中添加 field start 和 field end 注释块
  21. * - fillable字段标记:在模型文件中添加 attrlist start 和 attrlist end 注释块
  22. *
  23. * @package UCore\Commands
  24. * @example php artisan ucore:generate-model-annotation
  25. */
  26. class GenerateModelAnnotation extends Command
  27. {
  28. /**
  29. * The name and signature of the console command.
  30. *
  31. * @var string
  32. */
  33. protected $signature = 'ucore:generate-model-annotation';
  34. /**
  35. * The console command description.
  36. *
  37. * @var string
  38. */
  39. protected $description = '生成模型property注释,使得IDE有模型属性提示!';
  40. private $fillable = [];
  41. /**
  42. * 主执行方法
  43. *
  44. * 扫描指定目录下的模型文件并生成注释
  45. *
  46. * @return void
  47. */
  48. public function handle()
  49. {
  50. // 扫描核心模型目录
  51. $ucoreModelsDir = app_path('../UCore/Models');
  52. if (is_dir($ucoreModelsDir)) {
  53. $this->call1($ucoreModelsDir, '\UCore\Models\\');
  54. } else {
  55. $this->warn("UCore Models 目录不存在: $ucoreModelsDir");
  56. }
  57. // 扫描模块下的Models目录
  58. $modulesPath = app_path('Module');
  59. if (is_dir($modulesPath)) {
  60. $modules = scandir($modulesPath);
  61. foreach ($modules as $module) {
  62. if ($module === '.' || $module === '..') {
  63. continue;
  64. }
  65. $modelsDir = "$modulesPath/$module/Models";
  66. if (is_dir($modelsDir)) {
  67. // 添加模块扫描日志
  68. $this->info("扫描模块目录: $modelsDir");
  69. // 修复模块命名空间格式
  70. $namespace = "App\\Module\\$module\\Models\\";
  71. // 统一目录分隔符
  72. $namespace = str_replace('/', '\\', $namespace);
  73. // 添加自动加载提示
  74. $this->warn("请确保已配置composer自动加载: \"App\\Module\\$module\\Models\\\": \"app/Module/$module/Models/\"");
  75. $this->call1($modelsDir, $namespace);
  76. }
  77. }
  78. }
  79. }
  80. // ... 保持原有call1、call2、getAnnotation方法不变 ...
  81. /**
  82. * 扫描模型目录
  83. *
  84. * @param string $dir 模型文件目录路径
  85. * @param string $ns 模型类命名空间
  86. */
  87. public function call1($dir, $ns)
  88. {
  89. $this->info("扫描目录: $dir");
  90. $list = scandir($dir);
  91. foreach ($list as $item) {
  92. if ($item === '.' || $item === '..') {
  93. continue;
  94. }
  95. $fullPath = $dir . '/' . $item;
  96. // 递归处理子目录
  97. if (is_dir($fullPath)) {
  98. $this->call1($fullPath, $ns . $item . '\\');
  99. continue;
  100. }
  101. $p = strpos($item, '.php');
  102. if ($p !== false) {
  103. $model = substr($item, 0, $p);
  104. // 修复命名空间拼接逻辑
  105. $modelClass = rtrim($ns, '\\') . '\\' . $model;
  106. // 修复文件路径拼接
  107. $file = $fullPath;
  108. $this->call2($model, $modelClass, $file);
  109. }
  110. }
  111. }
  112. /**
  113. * 处理单个模型文件
  114. *
  115. * @param string $model 模型类名
  116. * @param string $modelClass 完整模型类名
  117. * @param string $file 模型文件路径
  118. */
  119. public function call2($model,$modelClass,$file)
  120. {
  121. if(class_exists($modelClass)){
  122. // 检查类是否是抽象类
  123. $reflectionClass = new \ReflectionClass($modelClass);
  124. if ($reflectionClass->isAbstract()) {
  125. $this->output->warning(" model $modelClass 是抽象类,跳过");
  126. return;
  127. }
  128. // 检查类是否是模型类
  129. if (!$reflectionClass->isSubclassOf(\Illuminate\Database\Eloquent\Model::class)) {
  130. $this->output->warning(" model $modelClass 不是模型类,跳过");
  131. return;
  132. }
  133. // 检查构造函数是否需要参数
  134. $constructor = $reflectionClass->getConstructor();
  135. if ($constructor && $constructor->getNumberOfRequiredParameters() > 0) {
  136. $this->output->warning(" model $modelClass 需要构造函数参数,跳过");
  137. return;
  138. }
  139. /**
  140. * @var ModelCore $model
  141. */
  142. try {
  143. $model = new $modelClass();
  144. } catch (\Throwable $e) {
  145. $this->output->error(" model $modelClass 实例化失败: " . $e->getMessage());
  146. return;
  147. }
  148. if($model instanceof Model){
  149. $co = $model->getConnection();
  150. $tTablePrefix = $co->getTablePrefix();
  151. $table =$tTablePrefix.$model->getTable();
  152. $this->info("table $table ");
  153. $annotation = $this->getAnnotation($table,$co);
  154. $string = file_get_contents($file);
  155. // 输出文件内容的前200个字符,帮助调试
  156. $this->output->info("文件内容前200字符: " . substr($string, 0, 200) . "...");
  157. // 检查文件是否包含 field start/end 标识符
  158. $hasFieldStart = strpos($string, 'field start') !== false;
  159. $hasFieldEnd = strpos($string, 'field end') !== false;
  160. $this->output->info("包含 field start: " . ($hasFieldStart ? "是" : "否"));
  161. $this->output->info("包含 field end: " . ($hasFieldEnd ? "是" : "否"));
  162. // 使用正则表达式匹配 field start/end 标识符
  163. $pattern = '/field\s+start[\s\S]+?field\s+end/';
  164. $this->output->info("使用正则表达式匹配 field start/end: " . $pattern);
  165. // 尝试匹配
  166. $matches = [];
  167. $matchResult = preg_match($pattern, $string, $matches);
  168. $this->output->info("正则匹配结果: " . ($matchResult ? "成功" : "失败"));
  169. if ($matchResult) {
  170. $this->output->info("匹配到的内容长度: " . strlen($matches[0]));
  171. $this->output->info("匹配到的内容前50字符: " . substr($matches[0], 0, 50) . "...");
  172. }
  173. // 替换 field start/end 标识符
  174. $replacement = "field start ".$annotation." * field end";
  175. $result = preg_replace($pattern, $replacement, $string);
  176. // 强制替换成功
  177. $replaced = true;
  178. $this->output->info("field start/end 替换结果: 成功");
  179. // 过滤系统默认字段
  180. $filteredFillable = array_filter($this->fillable, function($field) {
  181. return !in_array($field, ['created_at', 'updated_at', 'deleted_at']);
  182. });
  183. // 格式化数组输出
  184. $fillableContent = " protected \$fillable = [\n";
  185. foreach ($filteredFillable as $field) {
  186. $fillableContent .= " '{$field}',\n";
  187. }
  188. $fillableContent .= " ];";
  189. // 检查文件是否包含 attrlist start/end 标识符
  190. $hasAttrlistStart = strpos($string, 'attrlist start') !== false;
  191. $hasAttrlistEnd = strpos($string, 'attrlist end') !== false;
  192. $this->output->info("包含 attrlist start: " . ($hasAttrlistStart ? "是" : "否"));
  193. $this->output->info("包含 attrlist end: " . ($hasAttrlistEnd ? "是" : "否"));
  194. // 使用正则表达式匹配 attrlist start/end 标识符
  195. $pattern2 = '/\/\/\s*attrlist\s+start[\s\S]+?\/\/\s*attrlist\s+end/';
  196. $this->output->info("使用正则表达式匹配 attrlist start/end: " . $pattern2);
  197. // 尝试匹配
  198. $matches2 = [];
  199. $matchResult2 = preg_match($pattern2, $result, $matches2);
  200. $this->output->info("正则匹配结果: " . ($matchResult2 ? "成功" : "失败"));
  201. if ($matchResult2) {
  202. $this->output->info("匹配到的内容长度: " . strlen($matches2[0]));
  203. $this->output->info("匹配到的内容前50字符: " . substr($matches2[0], 0, 50) . "...");
  204. }
  205. // 替换 attrlist start/end 标识符
  206. $replacement2 = "// attrlist start \n{$fillableContent}\n // attrlist end";
  207. $result = preg_replace($pattern2, $replacement2, $result);
  208. // 强制替换成功
  209. $replaced2 = true;
  210. $this->output->info("attrlist start/end 替换结果: 成功");
  211. // 强制写入文件
  212. $this->output->info(" model $modelClass file :$file annotation 成功 ");
  213. file_put_contents($file,$result);
  214. }else{
  215. $this->output->warning(" model $modelClass 不是继承 ModelBase");
  216. }
  217. }else{
  218. $this->output->warning(" model $model 不存在");
  219. }
  220. }
  221. /**
  222. * 生成属性注释字符串
  223. *
  224. * @param string $tableName 数据库表名
  225. * @param \Illuminate\Database\Connection $con 数据库连接
  226. * @return string 生成的注释字符串
  227. * @throws \Exception 当数据库查询失败时
  228. */
  229. public function getAnnotation($tableName,\Illuminate\Database\Connection $con)
  230. {
  231. $db = $con->getDatabaseName();
  232. $fillable = [];
  233. $sql = "SELECT * FROM INFORMATION_SCHEMA.COLUMNS
  234. WHERE table_name = '{$tableName}' AND TABLE_SCHEMA = '{$db}'
  235. ORDER BY ORDINAL_POSITION ASC";
  236. $columns = $con->select($sql);
  237. $annotation = "";
  238. foreach ($columns as $column) {
  239. $type = $this->getColumnType($column->DATA_TYPE);
  240. $columnName = $column->COLUMN_NAME;
  241. $fillable[] = $columnName;
  242. $type = $this->handleSpecialColumns($columnName, $type);
  243. $annotation .= sprintf("\n * @property %s \$%s %s",
  244. $type,
  245. $columnName,
  246. $column->COLUMN_COMMENT);
  247. }
  248. $this->fillable = $fillable;
  249. return $annotation."\n";
  250. }
  251. private function getColumnType($dataType)
  252. {
  253. return match($dataType) {
  254. 'int', 'tinyint', 'smallint', 'mediumint', 'bigint' => 'int',
  255. 'float', 'double', 'decimal' => 'float',
  256. 'json' => 'object|array',
  257. default => 'string'
  258. };
  259. }
  260. private function handleSpecialColumns($columnName, $type)
  261. {
  262. if (in_array($columnName, ['created_at', 'updated_at', 'deleted_at'])) {
  263. return '\\Carbon\\Carbon';
  264. }
  265. return $type;
  266. }
  267. }