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

miaoxing / plugin / 7048390524

30 Nov 2023 03:02PM UTC coverage: 39.661% (+0.5%) from 39.147%
7048390524

push

github

twinh
ci: update package version

936 of 2360 relevant lines covered (39.66%)

18.18 hits per line

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

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

3
namespace Miaoxing\Plugin\Command;
4

5
use Miaoxing\Plugin\BasePlugin;
6
use Nette\PhpGenerator\ClassType;
7
use Nette\PhpGenerator\Method;
8
use Nette\PhpGenerator\Parameter;
9
use Nette\PhpGenerator\PhpFile;
10
use Nette\PhpGenerator\PhpNamespace;
11
use Nette\PhpGenerator\PsrPrinter;
12
use ReflectionClass;
13
use ReflectionMethod;
14
use Symfony\Component\Console\Input\InputArgument;
15

16
/**
17
 * 生成自动完成的代码文件
18
 *
19
 * 可行方案
20
 * 1. FILE_MODE_SINGLE + excludeParentMethods=false (推荐)
21
 * - PHPStorm 识别稳定
22
 * - 生成代码多
23
 *
24
 * 2. FILE_MODE_BY_TYPE + excludeParentMethods=true
25
 * - PHPStorm 识别不稳定,多次重启后能识别到
26
 * - 生成代码少
27
 *
28
 * 3. FILE_MODE_BY_CLASS
29
 * - 暂无区别,未实现
30
 *
31
 * @mixin \PluginPropMixin
32
 * @mixin \ClassMapMixin
33
 * @mixin \StrMixin
34
 * @see StaticCallTest
35
 * @SuppressWarnings(PHPMD.CouplingBetweenObjects)
36
 */
37
class GAutoCompletion extends BaseCommand
38
{
39
    use PluginIdTrait;
40

41
    /**
42
     * 所有的类生成一个文件
43
     */
44
    public const FILE_MODE_SINGLE = 1;
45

46
    /**
47
     * 生成两个文件,一个存放静态方法,一个存放动态方法
48
     */
49
    public const FILE_MODE_BY_TYPE = 2;
50

51
    /**
52
     * 每个类生成一个文件
53
     */
54
    public const FILE_MODE_BY_CLASS = 3;
55

56
    protected static $defaultName = 'g:auto-completion';
57

58
    /**
59
     * @var bool
60
     */
61
    protected $generateEmptyClass = true;
62

63
    /**
64
     * @var bool
65
     */
66
    protected $excludeParentMethods = false;
67

68
    /**
69
     * @var bool
70
     */
71
    protected $addNoinspectionComment = false;
72

73
    /**
74
     * @var int
75
     */
76
    protected $fileMode = self::FILE_MODE_SINGLE;
77

78
    /**
79
     * Generate static calls code completion
80
     *
81
     * @param array $services
82
     * @param string $path
83
     * @throws \ReflectionException
84
     */
85
    public function generateStaticCalls(array $services, string $path)
86
    {
87
        $printer = new PsrPrinter();
×
88
        $staticFile = new PhpFile();
×
89
        $dynamicFile = new PhpFile();
×
90

91
        if ($this->addNoinspectionComment) {
×
92
            $staticFile->addComment('@noinspection PhpDocSignatureInspection')
×
93
                ->addComment('@noinspection PhpFullyQualifiedNameUsageInspection')
×
94
                ->addComment('@noinspection PhpInconsistentReturnPointsInspection');
×
95
        }
96

97
        foreach ($services as $name => $serviceClass) {
×
98
            // 忽略 trait
99
            if (!class_exists($serviceClass)) {
×
100
                continue;
×
101
            }
102

103
            $refClass = new ReflectionClass($serviceClass);
×
104

105
            $staticNamespace = $staticFile->addNamespace($refClass->getNamespaceName());
×
106
            $dynamicNamespace = $dynamicFile->addNamespace($refClass->getNamespaceName());
×
107

108
            $class = new ClassType($refClass->getShortName());
×
109
            // NOTE: 如果增加了继承,Service目录之外的子类没有代码提示(如果还无效不断重启直到生效)
110
            if ($this->excludeParentMethods && $parent = $refClass->getParentClass()) {
×
111
                $class->addExtend($parent->getName());
×
112
            }
113

114
            $staticClass = clone $class;
×
115

116
            $methods = [];
×
117
            $staticMethods = [];
×
118
            $see = '@see ' . $refClass->getShortName() . '::';
×
119
            foreach ($refClass->getMethods(ReflectionMethod::IS_PROTECTED) as $refMethod) {
×
120
                // NOTE: 单文件下,如果排除了父类方法,第二级的子类(例如AppModel)没有代码提示
121
                if ($this->excludeParentMethods && $refMethod->getDeclaringClass()->getName() !== $serviceClass) {
×
122
                    continue;
×
123
                }
124

125
                if ($this->isApi($refMethod)) {
×
126
                    // NOTE: 使用注释,PHPStorm 也不会识别为动态调用
127
                    $method = Method::from([$serviceClass, $refMethod->getName()])->setPublic();
×
128

129
                    $see = '@see ' . $refMethod->getDeclaringClass()->getShortName() . '::' . $refMethod->getName();
×
130
                    $method->setComment(str_replace('@svc', $see, $method->getComment()));
×
131

132
                    $methods[] = $method;
×
133
                    $staticMethods[] = (clone $method)->setStatic();
×
134
                }
135
            }
136

137
            if ($this->generateEmptyClass || $staticMethods) {
×
138
                $staticClass->setMethods($staticMethods);
×
139
                $staticNamespace->add($staticClass);
×
140
            }
141
            if ($this->generateEmptyClass || $methods) {
×
142
                // NOTE: 分多个文件反而出现第二,三级的子类(例如AppModel)没有代码提示,魔术方法识别失败等问题
143
                $class->setMethods($methods);
×
144
                $dynamicNamespace->add($class);
×
145
            }
146
        }
147

148
        $this->addValidatorMethods($services, $staticFile, $dynamicFile);
×
149

150
        if (!isset($staticNamespace) || !$staticNamespace->getClasses()) {
×
151
            $this->suc('API method not found!');
×
152
            return;
×
153
        }
154

155
        switch ($this->fileMode) {
×
156
            default:
157
            case self::FILE_MODE_SINGLE:
×
158
                $this->writeSingle($printer, $staticFile, $dynamicFile, $path);
×
159
                break;
×
160

161
            case self::FILE_MODE_BY_TYPE:
×
162
                $this->writeByType($printer, $staticFile, $dynamicFile, $path);
×
163
                break;
×
164

165
            case self::FILE_MODE_BY_CLASS:
×
166
                $this->writeByClass($printer, $staticFile, $dynamicFile, $path);
×
167
                break;
×
168
        }
169
    }
170

171
    /**
172
     * {@inheritdoc}
173
     */
174
    protected function configure()
175
    {
176
        $this->setDescription('Generate code auto completion for specified plugin')
×
177
            ->addArgument('plugin-id', InputArgument::OPTIONAL, 'The id of plugin');
×
178
    }
179

180
    /**
181
     * @return int
182
     * @throws \ReflectionException
183
     * @throws \Exception
184
     */
185
    protected function handle()
186
    {
187
        $id = $this->getPluginId();
×
188
        if ('wei' === $id) {
×
189
            [$services, $path] = $this->getWeiConfig();
×
190
        } else {
191
            $plugin = $this->plugin->getOneById($id);
×
192
            $path = $plugin->getBasePath();
×
193
            $services = $this->getServerMap($plugin);
×
194
        }
195

196
        // NOTE: 需生成两个文件,services.php 里的类才能正确跳转到源文件
197
        $this->generateServices($services, $path);
×
198
        $this->generateStaticCalls($services, $path);
×
199

200
        return $this->suc('创建成功');
×
201
    }
202

203
    protected function getWeiConfig()
204
    {
205
        // TODO
206
        // 1. ClassMap 服务支持 wei/lib 目录(无类型)
207
        // 2. wei/wei 增加 psr-4 配置?
208

209
        $services = [];
×
210
        $path = 'packages/wei/lib';
×
211

212
        $files = glob($path . '/*.php');
×
213
        foreach ($files as $file) {
×
214
            $name = basename($file, '.php');
×
215
            $services[lcfirst($name)] = 'Wei\\' . $name;
×
216
        }
217

218
        foreach ($services as $name => $class) {
×
219
            if ((new ReflectionClass($class))->isAbstract()) {
×
220
                unset($services[$name]);
×
221
            }
222
        }
223

224
        return [$services, 'packages/wei'];
×
225
    }
226

227
    /**
228
     * Generate services' auto completion
229
     *
230
     * Including
231
     * 1. Mixin classes
232
     * 2. Function calls, that is wei()->xxx
233
     * 3. Global variables
234
     *
235
     * @param array $services
236
     * @param string $path
237
     * @throws \ReflectionException
238
     */
239
    protected function generateServices(array $services, string $path)
240
    {
241
        $content = "<?php\n\n";
×
242
        $autoComplete = '';
×
243

244
        foreach ($services as $name => $class) {
×
245
            // 使用 @property 和 @method,PHPStorm 会识别出是动态调用,加粗调用的代码
246
            $docBlock = rtrim($this->generateDocBlock($name, $class));
×
247
            $className = ucfirst($name) . 'Mixin';
×
248
            $content .= $this->generateClass($className, $docBlock) . "\n";
×
249

250
            $autoComplete .= ' * @mixin ' . $className . "\n";
×
251

252
            $propDocBlock = rtrim($this->generateDocBlock($name, $class, false));
×
253
            $propClassName = ucfirst($name) . 'PropMixin';
×
254
            $content .= $this->generateClass($propClassName, $propDocBlock) . "\n";
×
255
        }
256

257
        $content .= $this->generateClass('AutoCompletion', rtrim($autoComplete));
×
258
        $content .= <<<'PHP'
×
259

260
/**
261
 * @return AutoCompletion|Wei\Wei
262
 */
263
function wei()
264
{
265
    return new AutoCompletion(func_get_args());
266
}
267

268
PHP;
×
269

270
        $this->createFile($path . '/docs/auto-completion.php', $content);
×
271
    }
272

273
    protected function writeSingle(PsrPrinter $printer, PhpFile $staticFile, PhpFile $dynamicFile, string $path)
274
    {
275
        $statics = $printer->printFile($staticFile);
×
276
        $dynamics = $printer->printFile($dynamicFile);
×
277

278
        // Remove first (<?php\n) line
279
        $dynamics = substr($dynamics, strpos($dynamics, "\n") + 1);
×
280

281
        // indent 4 spaces
282
        $lines = [];
×
283
        foreach (explode("\n", $dynamics) as $line) {
×
284
            $lines[] = $line ? ('    ' . $line) : '';
×
285
        }
286
        $dynamics = implode("\n", $lines);
×
287

288
        // Wrap `if (0) ` outside class definition
289
        $index = 0;
×
290
        $dynamics = preg_replace_callback('/    namespace (.+?)\n/mi', function ($matches) use (&$index) {
291
            ++$index;
×
292
            $prefix = 1 === $index ? '' : "\n}\n";
×
293
            return $prefix . ltrim($matches[0]) . "\nif (0) {";
×
294
        }, $dynamics);
×
295
        $dynamics .= "}\n";
×
296

297
        $content = $statics . $dynamics;
×
298
        $this->createFile($path . '/docs/auto-completion-static.php', $content);
×
299

300
        $file = $path . '/docs/auto-completion-dynamic.php';
×
301
        if (is_file($file)) {
×
302
            unlink($file);
×
303
        }
304
    }
305

306
    protected function writeByType(PsrPrinter $printer, PhpFile $staticFile, PhpFile $dynamicFile, string $path)
307
    {
308
        $statics = $printer->printFile($staticFile);
×
309
        $this->createFile($path . '/docs/auto-completion-static.php', $statics);
×
310

311
        $dynamics = $printer->printFile($dynamicFile);
×
312
        $this->createFile($path . '/docs/auto-completion-dynamic.php', $dynamics);
×
313
    }
314

315
    protected function writeByClass(PsrPrinter $printer, PhpFile $staticFile, PhpFile $dynamicFile, string $path)
316
    {
317
        throw new \RuntimeException('Not supported yet');
×
318
//        $header = $printer->printFile($staticFile) . "\n";
319
//        foreach ($statics as $name => $content) {
320
//            $this->createFile($path . '/docs/auto-completion-static-' . $name . '.php', $header . $content);
321
//        }
322
//        foreach ($dynamics as $name => $content) {
323
//            $this->createFile($path . '/docs/auto-completion-dynamic-' . $name . '.php', $header . $content);
324
//        }
325
    }
326

327
    /**
328
     * @param string $name
329
     * @param string $class
330
     * @param mixed $generateInvoke
331
     * @return string
332
     * @throws \ReflectionException
333
     */
334
    protected function generateDocBlock(string $name, string $class, $generateInvoke = true)
335
    {
336
        $docBlock = '';
×
337
        $ref = new ReflectionClass($class);
×
338
        $docName = $this->getDocCommentTitle($ref->getDocComment());
×
339

340
        $docBlock .= rtrim(sprintf(' * @property    %s $%s %s', $class, $name, $docName)) . "\n";
×
341

342
        if ($generateInvoke && method_exists($class, '__invoke')) {
×
343
            $method = $ref->getMethod('__invoke');
×
344
            $return = $this->getMethodReturn($ref, $method) ?: 'mixed';
×
345
            $methodName = $this->getDocCommentTitle($method->getDocComment()) ?: '';
×
346

347
            $params = $this->geParam($method);
×
348

349
            $docBlock .= rtrim(sprintf(' * @method      %s %s(%s) %s', $return, $name, $params, $methodName));
×
350
            $docBlock .= "\n";
×
351
        }
352

353
        return $docBlock;
×
354
    }
355

356
    protected function geParam(ReflectionMethod $method)
357
    {
358
        $params = $method->getParameters();
×
359
        if (!$params) {
×
360
            return '';
×
361
        }
362

363
        $string = '';
×
364
        foreach ($params as $param) {
×
365
            if ($string) {
×
366
                $string .= ', ';
×
367
            }
368

369
            $string .= '$' . $param->getName();
×
370
            if ($param->isDefaultValueAvailable()) {
×
371
                $string .= ' = ' . $this->convertParamValueToString($param->getDefaultValue());
×
372
            }
373
        }
374

375
        return $string;
×
376
    }
377

378
    protected function convertParamValueToString($value)
379
    {
380
        switch (gettype($value)) {
×
381
            case 'NULL':
×
382
                return 'null';
×
383

384
            case 'array':
×
385
                return '[]';
×
386

387
            default:
388
                return var_export($value, true);
×
389
        }
390
    }
391

392
    protected function getMethodReturn(ReflectionClass $class, ReflectionMethod $method)
393
    {
394
        $doc = $method->getDocComment();
×
395
        preg_match('/@return (.+?)\n/', $doc, $matches);
×
396
        if (!$matches) {
×
397
            return false;
×
398
        }
399

400
        $return = $matches[1];
×
401
        $className = $class->getName();
×
402

403
        $return = str_replace([
×
404
            'BaseModel',
×
405
            '$this',
×
406
        ], [
×
407
            $className,
×
408
            $className,
×
409
        ], $return);
×
410

411
        // 忽略空格后面的辅助说明
412
        if ($return) {
×
413
            $return = explode(' ', $return)[0];
×
414
        }
415

416
        return $return ?: false;
×
417
    }
418

419
    /**
420
     * 返回注释的标题(第一行)
421
     *
422
     * @param string $docComment
423
     * @return bool|mixed
424
     */
425
    protected function getDocCommentTitle($docComment)
426
    {
427
        preg_match('#\* ([^@]+?)\n#is', $docComment, $matches);
×
428
        if ($matches) {
×
429
            return $matches[1];
×
430
        }
431

432
        return false;
×
433
    }
434

435
    protected function createFile($file, $content)
436
    {
437
        $this->suc('生成文件 ' . $file);
×
438
        $this->createDir(dirname($file));
×
439
        file_put_contents($file, $content);
×
440
        chmod($file, 0777);
×
441
    }
442

443
    protected function createDir($dir)
444
    {
445
        if (!is_dir($dir)) {
×
446
            mkdir($dir, 0777, true);
×
447
            chmod($dir, 0777);
×
448
        }
449
    }
450

451
    /**
452
     * @param BasePlugin $plugin
453
     * @return array
454
     */
455
    protected function getServerMap(BasePlugin $plugin)
456
    {
457
        $basePath = $plugin->getBasePath() . '/src';
×
458

459
        return wei()->classMap->generate([$basePath], '/Service/*.php', 'Service', false);
×
460
    }
461

462
    protected function intent($content, $space = '    ')
463
    {
464
        $array = [];
×
465
        foreach (explode("\n", $content) as $line) {
×
466
            $array[] = $space . $line;
×
467
        }
468
        return implode("\n", $array);
×
469
    }
470

471
    protected function isApi(ReflectionMethod $method)
472
    {
473
        return strpos($method->getDocComment(), '* @svc');
×
474
    }
475

476
    protected function generateClass($class, $comment)
477
    {
478
        return <<<PHP
×
479
/**
×
480
$comment
×
481
 */
482
 #[\\AllowDynamicProperties]
483
class $class
×
484
{
485
}
486

487
PHP;
×
488
    }
489

490
    private function addValidatorMethods(array $services, PhpFile $staticFile, PhpFile $dynamicFile)
491
    {
492
        $validators = [];
×
493
        foreach ($services as $name => $class) {
×
494
            if ('is' === substr($name, 0, 2)) {
×
495
                $validators[$name] = $class;
×
496
            }
497
        }
498
        if (!$validators) {
×
499
            return;
×
500
        }
501

502
        $staticNamespace = $staticFile->addNamespace('Wei');
×
503
        $dynamicNamespace = $dynamicFile->addNamespace('Wei');
×
504
        $staticClass = $this->getOrAddClass($staticNamespace, 'V');
×
505
        $dynamicClass = $this->getOrAddClass($dynamicNamespace, 'V');
×
506

507
        $methods = $dynamicClass->getMethods();
×
508
        $staticMethods = $staticClass->getMethods();
×
509

510
        foreach ($validators as $name => $class) {
×
511
            $name = substr($name, 2);
×
512

513
            $dynamicMethod = Method::from([$class, '__invoke'])->cloneWithName(lcfirst($name));
×
514
            $staticMethod = clone $dynamicMethod;
×
515

516
            // 移除 $input 参数
517
            $parameters = $dynamicMethod->getParameters();
×
518
            array_shift($parameters);
×
519

520
            // 加上 key 和 label
521
            $nameParameter = new Parameter('key');
×
522
            $nameParameter->setDefaultValue(null);
×
523

524
            $labelParameter = new Parameter('label');
×
525
            $labelParameter->setType('string');
×
526
            $labelParameter->setDefaultValue(null);
×
527
            array_unshift($parameters, $nameParameter, $labelParameter);
×
528

529
            $dynamicMethod->setParameters($parameters);
×
530

531
            $dynamicMethod->setComment('@return $this');
×
532
            $dynamicMethod->addComment('@see \\' . $class . '::__invoke');
×
533

534
            $staticMethod->setComment('@return $this');
×
535
            $staticMethod->addComment('@see \\' . $class . '::__invoke');
×
536

537
            $methods[] = $dynamicMethod;
×
538
            $staticMethods[] = $staticMethod->setStatic();
×
539

540
            $methods[] = $dynamicMethod->cloneWithName('not' . $name);
×
541
            $staticMethods[] = $staticMethod->cloneWithName('not' . $name);
×
542
        }
543

544
        $dynamicClass->setMethods($methods);
×
545
        $staticClass->setMethods($staticMethods);
×
546
    }
547

548
    private function getOrAddClass(PhpNamespace $namespace, string $class)
549
    {
550
        $classes = $namespace->getClasses();
×
551
        return $classes[$class] ?? $namespace->addClass($class);
×
552
    }
553
}
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