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

miaoxing / plugin / 13068069565

31 Jan 2025 07:05AM UTC coverage: 41.294% (-0.06%) from 41.349%
13068069565

push

github

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

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

83 existing lines in 1 file now uncovered.

1238 of 2998 relevant lines covered (41.29%)

36.6 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 Symfony\Component\Console\Input\InputArgument;
9
use Symfony\Component\Console\Input\InputOption;
10
use Wei\BaseModel;
11
use Wei\Model\CamelCaseTrait;
12
use Wei\Model\Relation;
13
use Wei\ModelTrait;
14

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

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

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

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

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

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

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

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

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

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

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

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

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

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

126
        switch ($type) {
UNCOV
127
            case 'int':
×
UNCOV
128
            case 'smallint':
×
129
            case 'mediumint':
×
130
                return 'int';
×
131

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

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

148
            case 'json':
×
UNCOV
149
                return 'array';
×
150

151
            default:
UNCOV
152
                return $type;
×
153
        }
154
    }
155

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

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

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

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

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

UNCOV
195
        return $docBlocks;
×
196
    }
197

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

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

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

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

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

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

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

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

UNCOV
281
        return true;
×
282
    }
283

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

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

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

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

UNCOV
331
        return false;
×
332
    }
333

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

349
        $lines = explode("\n", $docComment);
×
350

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

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

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

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

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

UNCOV
388
        file_put_contents($file, $fileContent);
×
389
    }
390
}
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

© 2025 Coveralls, Inc