• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

miaoxing / plugin / 13066017523

26 Jan 2025 04:40AM UTC coverage: 41.349% (-1.1%) from 42.441%
13066017523

push

github

twinh
feat(plugin, experimental): 增加 `PresetColumns` 服务,用于生成常用的字段

0 of 6 new or added lines in 1 file covered. (0.0%)

147 existing lines in 4 files now uncovered.

1238 of 2994 relevant lines covered (41.35%)

36.64 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/Command/GMetadata.php
1
<?php
2

3
namespace Miaoxing\Plugin\Command;
4

5
use phpDocumentor\Reflection\DocBlock;
6
use phpDocumentor\Reflection\DocBlock\Tags\Property;
7
use phpDocumentor\Reflection\DocBlockFactory;
8
use ReflectionClass;
9
use ReflectionMethod;
10
use Symfony\Component\Console\Input\InputArgument;
11
use Symfony\Component\Console\Input\InputOption;
12
use Wei\BaseModel;
13
use Wei\Model\CamelCaseTrait;
14
use Wei\Model\Relation;
15
use Wei\ModelTrait;
16

17
/**
18
 * @mixin \StrPropMixin
19
 * @mixin \ClsPropMixin
20
 * @mixin \PluginPropMixin
21
 * @mixin \ClassMapPropMixin
22
 * @experimental will refactor to add more features
23
 */
24
final class GMetadata extends BaseCommand
25
{
26
    public function handle()
27
    {
28
        $pluginIds = [];
×
UNCOV
29
        $pluginId = $this->getArgument('plugin-id');
×
UNCOV
30
        if ($pluginId) {
×
31
            $pluginIds[] = $pluginId;
×
32
        } else {
33
            foreach ($this->plugin->getAll() as $plugin) {
×
34
                $pluginIds[] = $plugin->getId();
×
35
            }
36
        }
37

38
        foreach ($pluginIds as $pluginId) {
×
39
            $plugin = $this->plugin->getOneById($pluginId);
×
40
            $services = $this->classMap->generate(
×
41
                [$plugin->getBasePath() . '/src'],
×
UNCOV
42
                '/Service/?*Model.php', // 排除 Model.php
×
UNCOV
43
                'Service',
×
44
                false
×
UNCOV
45
            );
×
UNCOV
46
            foreach ($services as $class) {
×
UNCOV
47
                if (!is_subclass_of($class, BaseModel::class)) {
×
UNCOV
48
                    continue;
×
49
                }
50
                $this->updateClass($class);
×
51
            }
52
        }
53

UNCOV
54
        $this->suc('创建成功');
×
55
    }
56

57
    protected function configure()
58
    {
59
        $this
×
UNCOV
60
            ->addArgument('plugin-id', InputArgument::OPTIONAL, 'The id of plugin')
×
61
            ->addOption('rewrite', 'r', InputOption::VALUE_NONE, 'Whether to rewrite the existing metadata');
×
62
    }
63

64
    protected function updateClass(string $modelClass)
65
    {
66
        /** @var BaseModel $model */
UNCOV
67
        $model = new $modelClass();
×
68
        $reflectionClass = new ReflectionClass($model);
×
69
        $camelCase = $this->cls->usesDeep($modelClass)[CamelCaseTrait::class] ?? false;
×
70

71
        // 生成表格字段的属性的注释
72
        $docBlocks = $this->getDocBlocksFromTable($model, $camelCase);
×
73

74
        // 生成 getXxxAttribute 的方法定义的属性的注释
UNCOV
75
        $docBlocks = array_merge($docBlocks, $this->getDocBlocksFromAccessors($reflectionClass, $camelCase));
×
76

77
        // 获取关联的定义
78
        $docBlocks = array_merge($docBlocks, $this->getDocBlocksFromRelationMethods($reflectionClass));
×
79

80
        $docComment = $reflectionClass->getDocComment();
×
81
        $factory = DocBlockFactory::createInstance();
×
82
        if ($docComment) {
×
UNCOV
83
            $docblock = $factory->create($docComment);
×
84
        } else {
UNCOV
85
            $docblock = new DocBlock();
×
86
        }
87

88
        $properties = [];
×
89
        foreach ($docblock->getTags() as $tag) {
×
90
            if (!$tag instanceof Property) {
×
91
                continue;
×
92
            }
UNCOV
93
            $properties[$tag->getVariableName()] = $tag;
×
94
        }
95

96
        // 没有的新增
97
        $new = [];
×
98
        foreach ($docBlocks as $propertyName => $docBlock) {
×
UNCOV
99
            if (!isset($properties[$propertyName])) {
×
UNCOV
100
                $new[$propertyName] = $docBlock;
×
101
            }
102
        }
103
        $docComment = $this->addDocComment($docComment, implode("\n", $new));
×
104

105
        // 已有的重写
UNCOV
106
        if ($this->getOption('rewrite')) {
×
UNCOV
107
            foreach ($docblock->getTags() as $tag) {
×
UNCOV
108
                if (!$tag instanceof Property) {
×
109
                    continue;
×
110
                }
111
                if (isset($docBlocks[$tag->getVariableName()])) {
×
UNCOV
112
                    $docComment = str_replace(' * @property ' . $tag, $docBlocks[$tag->getVariableName()], $docComment);
×
113
                }
114
            }
115
        }
116

117
        // 写入文件
UNCOV
118
        $this->updateDocComment($reflectionClass, $docComment);
×
119
        $this->suc('更新文件 ' . $reflectionClass->getFileName());
×
120
    }
121

122
    protected function getPhpType($columnType)
123
    {
124
        $parts = explode('(', $columnType);
×
125
        $type = $parts[0];
×
126
        $length = (int) ($parts[1] ?? 0);
×
127

128
        switch ($type) {
129
            case 'int':
×
130
            case 'smallint':
×
131
            case 'mediumint':
×
132
                return 'int';
×
133

UNCOV
134
            case 'tinyint':
×
135
                return 1 === $length ? 'bool' : 'int';
×
136

UNCOV
137
            case 'bigint':
×
UNCOV
138
            case 'varchar':
×
139
            case 'char':
×
UNCOV
140
            case 'mediumtext':
×
UNCOV
141
            case 'text':
×
UNCOV
142
            case 'timestamp':
×
UNCOV
143
            case 'datetime':
×
UNCOV
144
            case 'date':
×
145
            case 'decimal':
×
146
            case 'binary':
×
UNCOV
147
            case 'varbinary':
×
UNCOV
148
                return 'string';
×
149

150
            case 'json':
×
UNCOV
151
                return 'array';
×
152

153
            default:
UNCOV
154
                return $type;
×
155
        }
156
    }
157

158
    /**
159
     * 生成表格字段的属性的注释
160
     *
161
     * @param BaseModel $modelObject
162
     * @param bool $camelCase
163
     * @return array
164
     */
165
    protected function getDocBlocksFromTable(BaseModel $modelObject, bool $camelCase): array
166
    {
UNCOV
167
        $table = $modelObject->getDb()->getTable($modelObject->getTable());
×
UNCOV
168
        $columns = wei()->db->fetchAll('SHOW FULL COLUMNS FROM ' . $table);
×
UNCOV
169
        $modelColumns = $modelObject->getColumns();
×
170

171
        $docBlocks = [];
×
172
        foreach ($columns as $column) {
×
UNCOV
173
            $propertyName = $camelCase ? $this->str->camel($column['Field']) : $column['Field'];
×
174
            $cast = $modelColumns[$propertyName]['cast'] ?? null;
×
175

UNCOV
176
            if ('list' === $cast || 'list' === ($cast[0] ?? null)) {
×
UNCOV
177
                $phpType = 'array';
×
UNCOV
178
            } elseif ('object' === $cast) {
×
179
                $phpType = 'object';
×
180
            } else {
181
                $phpType = $this->getPhpType($column['Type']);
×
182
            }
183

184
            if (isset($modelColumns[$propertyName]['nullable']) && $modelColumns[$propertyName]['nullable']) {
×
185
                $phpType .= '|null';
×
186
            }
187

188
            $propertyName = $camelCase ? $this->str->camel($column['Field']) : $column['Field'];
×
UNCOV
189
            $docBlocks[$propertyName] = rtrim(sprintf(
×
UNCOV
190
                ' * @property %s $%s %s',
×
UNCOV
191
                $phpType,
×
UNCOV
192
                $propertyName,
×
193
                $column['Comment']
×
194
            ));
×
195
        }
196

UNCOV
197
        return $docBlocks;
×
198
    }
199

200
    /**
201
     * 生成 getXxxAttribute 的方法定义的属性的注释
202
     *
203
     * @param ReflectionClass $reflectionClass
204
     * @param bool $camelCase
205
     * @return array
206
     */
207
    protected function getDocBlocksFromAccessors(ReflectionClass $reflectionClass, bool $camelCase): array
208
    {
UNCOV
209
        $docBlocks = [];
×
210
        preg_match_all('/(?<=^|;)get([^;]+?)Attribute(;|$)/', implode(';', get_class_methods($reflectionClass->getName())), $matches);
×
211
        foreach ($matches[1] as $key => $attr) {
×
212
            $propertyName = $camelCase ? lcfirst($attr) : $this->str->snake($attr);
×
UNCOV
213
            if (isset($docBlocks[$propertyName])) {
×
UNCOV
214
                continue;
×
215
            }
216

UNCOV
217
            $method = rtrim($matches[0][$key], ';');
×
UNCOV
218
            $reflectionMethod = $reflectionClass->getMethod($method);
×
UNCOV
219
            $name = $this->getDocCommentTitle($reflectionMethod->getDocComment());
×
UNCOV
220
            $return = $this->getMethodReturn($reflectionMethod);
×
UNCOV
221
            $docBlocks[$propertyName] = rtrim(sprintf(' * @property %s $%s %s', $return, $propertyName, $name));
×
222
        }
UNCOV
223
        return $docBlocks;
×
224
    }
225

226
    /**
227
     * 生成关联方法的注释
228
     *
229
     * @param ReflectionClass $reflectionClass
230
     * @return array
231
     */
232
    protected function getDocBlocksFromRelationMethods(ReflectionClass $reflectionClass): array
233
    {
UNCOV
234
        $properties = [];
×
UNCOV
235
        foreach ($reflectionClass->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
×
UNCOV
236
            if ($this->isRelation($method, $reflectionClass)) {
×
UNCOV
237
                $propertyName = $method->getName();
×
UNCOV
238
                $returnName = $this->getMethodReturn($method);
×
UNCOV
239
                $properties[$propertyName] = rtrim(sprintf(
×
UNCOV
240
                    ' * @property %s $%s %s',
×
UNCOV
241
                    $returnName,
×
UNCOV
242
                    $propertyName,
×
UNCOV
243
                    $this->getDocCommentTitle($method->getDocComment())
×
UNCOV
244
                ));
×
245
            }
246
        }
UNCOV
247
        return $properties;
×
248
    }
249

250
    protected function isRelation(ReflectionMethod $method, ReflectionClass $reflectionClass): bool
251
    {
252
        // PHP 8
UNCOV
253
        if (method_exists($method, 'getAttributes') && $method->getAttributes(Relation::class)) {
×
UNCOV
254
            return true;
×
255
        }
256

257
        // Compat with PHP less than 8
UNCOV
258
        if (false !== strpos($method->getDocComment() ?: '', '@Relation')) {
×
UNCOV
259
            return true;
×
260
        }
261

UNCOV
262
        $returnType = $method->getReturnType();
×
UNCOV
263
        if (!$returnType) {
×
UNCOV
264
            return false;
×
265
        }
266

UNCOV
267
        if (!is_subclass_of($returnType->getName(), BaseModel::class)) {
×
UNCOV
268
            return false;
×
269
        }
270

271
        // 跳过 ModelTrait 和父类方法
UNCOV
272
        if (method_exists(ModelTrait::class, $method->getName())) {
×
UNCOV
273
            return false;
×
274
        }
UNCOV
275
        if ($method->getDeclaringClass()->getName() !== $reflectionClass->getName()) {
×
UNCOV
276
            return false;
×
277
        }
278

UNCOV
279
        return true;
×
280
    }
281

282
    /**
283
     * 获取方法的返回类型,优先从注释获取,其次是方法的返回类型
284
     *
285
     * @param ReflectionMethod $method
286
     * @return string
287
     */
288
    protected function getMethodReturn(ReflectionMethod $method): string
289
    {
UNCOV
290
        $docComment = $method->getDocComment();
×
UNCOV
291
        if ($docComment) {
×
292
            // 不使用 PHPDoc 解析,因为类名默认会加上全局命名空间
UNCOV
293
            preg_match('/@return (.+?)\s/', $docComment, $matches);
×
UNCOV
294
            if ($matches) {
×
UNCOV
295
                return $matches[1];
×
296
            }
297
        }
298

UNCOV
299
        $returnType = $method->getReturnType();
×
UNCOV
300
        if (!$returnType) {
×
UNCOV
301
            return 'mixed';
×
302
        }
303

304
        // 使用静态解析,不使用反射,因为返回值会包含命名空间,实际命名空间已经导入了
UNCOV
305
        $startLine = $method->getStartLine();
×
UNCOV
306
        $endLine = $method->getEndLine();
×
UNCOV
307
        $source = file($method->getFileName());
×
UNCOV
308
        $methodCode = implode('', array_slice($source, $startLine - 1, $endLine - $startLine + 1));
×
UNCOV
309
        preg_match('/\)\s*:\s*(.+?)\s/', $methodCode, $matches);
×
UNCOV
310
        if ($matches) {
×
UNCOV
311
            return $matches[1];
×
312
        }
UNCOV
313
        return 'mixed';
×
314
    }
315

316
    /**
317
     * 返回注释的标题(第一行)
318
     *
319
     * @param string $docComment
320
     * @return bool|mixed
321
     */
322
    protected function getDocCommentTitle($docComment)
323
    {
UNCOV
324
        preg_match('#\* ([^@]+?)\n#is', $docComment, $matches);
×
UNCOV
325
        if ($matches) {
×
UNCOV
326
            return $matches[1];
×
327
        }
328

UNCOV
329
        return false;
×
330
    }
331

332
    /**
333
     * 往类注释插入新的内容
334
     *
335
     * 内容需以 `* ` 开头
336
     *
337
     * @param string $docComment
338
     * @param string $newDocBlock
339
     * @return string
340
     */
341
    protected function addDocComment(string $docComment, string $newDocBlock): string
342
    {
UNCOV
343
        if (!$newDocBlock) {
×
UNCOV
344
            return $docComment;
×
345
        }
346

UNCOV
347
        $lines = explode("\n", $docComment);
×
348

UNCOV
349
        if (count($lines) > 1) {
×
UNCOV
350
            array_splice($lines, -1, 0, $newDocBlock);
×
351
        } else {
UNCOV
352
            $lines = [
×
UNCOV
353
                '/**',
×
UNCOV
354
                $newDocBlock,
×
UNCOV
355
                ' */',
×
UNCOV
356
            ];
×
357
        }
358

359
        // 将数组重新组合成字符串并返回
UNCOV
360
        return implode("\n", $lines);
×
361
    }
362

363
    /**
364
     * 更新类注释为新的内容
365
     *
366
     * @param ReflectionClass $reflectionClass
367
     * @param string $newDocComment
368
     * @return void
369
     */
370
    protected function updateDocComment(ReflectionClass $reflectionClass, string $newDocComment)
371
    {
UNCOV
372
        $file = $reflectionClass->getFileName();
×
UNCOV
373
        $fileContent = file_get_contents($file);
×
374

UNCOV
375
        $docComment = $reflectionClass->getDocComment();
×
UNCOV
376
        if (!$docComment) {
×
UNCOV
377
            $startLine = $reflectionClass->getStartLine();
×
UNCOV
378
            $lines = explode(PHP_EOL, $fileContent);
×
379

UNCOV
380
            array_splice($lines, $startLine - 1, 0, [$newDocComment]);
×
UNCOV
381
            $fileContent = implode(PHP_EOL, $lines);
×
382
        } else {
UNCOV
383
            $fileContent = str_replace($docComment, $newDocComment, $fileContent);
×
384
        }
385

UNCOV
386
        file_put_contents($file, $fileContent);
×
387
    }
388
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc