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

miaoxing / plugin / 5712595623

pending completion
5712595623

push

github

twinh
feat(plugin): 安装插件时,同时安装依赖的插件

0 of 30 new or added lines in 3 files covered. (0.0%)

2 existing lines in 2 files now uncovered.

918 of 2349 relevant lines covered (39.08%)

18.3 hits per line

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

34.54
/src/Service/Plugin.php
1
<?php
2

3
namespace Miaoxing\Plugin\Service;
4

5
use Exception;
6
use Miaoxing\Plugin\BaseService;
7

8
/**
9
 * 插件管理器
10
 *
11
 * 注意: 启用调试模式下,Ctrl+F5可以刷新缓存
12
 *
13
 * @mixin \CacheMixin 常规缓存,用于记录插件对应的事件
14
 * @property \Wei\BaseCache $configCache 记录生成的配置数组,使用phpFileCache速度最快,但需要在开发过程中生成,
15
 *                                       或者线上服务器可写,否则可改为memcached,redis等缓存
16
 * @mixin \EventMixin
17
 * @mixin \ReqMixin
18
 * @mixin \AppMixin
19
 * @mixin \ClassMapMixin
20
 * @mixin \PageRouterMixin
21
 */
22
class Plugin extends BaseService
23
{
24
    /**
25
     * The default priority for plugin event
26
     */
27
    protected const DEFAULT_PRIORITY = 100;
28

29
    /**
30
     * 插件所在的目录,允许使用通配符
31
     *
32
     * @var array
33
     */
34
    protected $basePaths = [
35
        'src',
36
        'plugins/*/src',
37
    ];
38

39
    /**
40
     * Whether enable plugin class autoload or not
41
     *
42
     * @var bool
43
     */
44
    protected $autoload = true;
45

46
    /**
47
     * The service names to ignore when generating the plugin config cache
48
     *
49
     * @var string[]
50
     */
51
    protected $ignoredServices = [
52
        'snowflake',
53
    ];
54

55
    /**
56
     * A List of build-in plugins
57
     *
58
     * @var array
59
     */
60
    protected $builtIns = ['plugin'];
61

62
    /**
63
     * An array that stores plugin classes,
64
     * the key is plugin ID and value is plugin class name
65
     *
66
     * @var array
67
     */
68
    protected $pluginClasses = [];
69

70
    /**
71
     * The instanced plugin objects
72
     *
73
     * @var array
74
     */
75
    protected $pluginInstances = [];
76

77
    /**
78
     * 插件的事件缓存数组
79
     *
80
     * @var array
81
     */
82
    protected $events = [];
83

84
    /**
85
     * 插件事件是否已绑定的标志位
86
     *
87
     * @var array
88
     */
89
    protected $loadedEvents = [];
90

91
    /**
92
     * {@inheritdoc}
93
     */
94
    protected $providers = [
95
        'configCache' => 'phpFileCache',
96
    ];
97

98
    /**
99
     * {@inheritdoc}
100
     */
101
    public function __construct(array $options = [])
102
    {
103
        // Trigger setAutoload
104
        if (!isset($options['autoload'])) {
3✔
105
            $options['autoload'] = $this->autoload;
3✔
106
        }
107

108
        parent::__construct($options);
3✔
109

110
        // If the plugin service is not constructed, the service container can't set config for it
111
        if (!$this->wei->isInstanced('plugin')) {
3✔
112
            $this->wei->set('plugin', $this);
×
113
        }
114

115
        // Load configs to services
116
        $this->loadConfig();
3✔
117
    }
1✔
118

119
    /**
120
     * Whether enable autoload or not
121
     *
122
     * @param bool $autoload
123
     * @return $this
124
     */
125
    public function setAutoload($autoload)
126
    {
127
        $this->autoload = (bool) $autoload;
3✔
128
        call_user_func(
3✔
129
            $autoload ? 'spl_autoload_register' : 'spl_autoload_unregister',
3✔
130
            [$this, 'autoload']
3✔
131
        );
2✔
132
        return $this;
3✔
133
    }
134

135
    /**
136
     * Receive plugin relatives service configs
137
     *
138
     * @param bool $refresh
139
     * @return array
140
     */
141
    public function getConfig($refresh = false)
142
    {
143
        return $this->getCache('plugins-config', $refresh, function () {
2✔
144
            return [
145
                'wei' => [
146
                    'aliases' => $this->getWeiAliases(),
×
147
                    'preload' => $this->getWeiPreload(),
×
148
                ],
149
                'plugin' => [
150
                    'pluginClasses' => $this->getPluginClasses(true),
×
151
                ],
152
                'pageRouter' => [
153
                    'pages' => $this->pageRouter->generatePages(),
×
154
                ],
155
            ];
156
        });
3✔
157
    }
158

159
    /**
160
     * 将@开头的文件路径,转换为真实的路径
161
     *
162
     * @param string $file
163
     * @return string
164
     */
165
    public function locateFile($file)
166
    {
167
        $components = $this->parseResource($file);
×
168
        if ($components['path']) {
×
169
            $path = dirname($components['path']) . '/public/';
×
170

171
            return $path . $components['file'];
×
172
        } else {
173
            return $components['file'];
×
174
        }
175
    }
176

177
    /**
178
     * Parse a resource and return the components contains path and file
179
     *
180
     * @param string $resource
181
     * @return array|false Returns false when resource is not starts with @
182
     */
183
    public function parseResource($resource)
184
    {
185
        $pluginId = $file = null;
12✔
186
        if (isset($resource[0]) && '@' == $resource[0]) {
12✔
187
            list($pluginId, $file) = explode('/', $resource, 2);
3✔
188
            $pluginId = substr($pluginId, 1);
3✔
189
        }
190

191
        if ($pluginId) {
12✔
192
            $plugin = $this->getOneById($pluginId);
3✔
193
            $path = $plugin->getBasePath() . '/views';
3✔
194

195
            return ['path' => $path, 'file' => $file];
3✔
196
        } else {
197
            return ['path' => null, 'file' => $resource];
12✔
198
        }
199
    }
200

201
    /**
202
     * Parse a view resource
203
     *
204
     * @param string $resource
205
     * @return array
206
     */
207
    public function parseViewResource($resource)
208
    {
209
        $components = $this->parseResource($resource);
12✔
210
        if ($components['path']) {
12✔
211
            $components['path'] .= '/';
3✔
212
        }
213

214
        return $components;
12✔
215
    }
216

217
    /**
218
     * 获取插件目录下所有的插件对象
219
     *
220
     * @return \Miaoxing\Plugin\BasePlugin[]
221
     */
222
    public function getAll()
223
    {
224
        $data = [];
×
225
        foreach ($this->pluginClasses as $id => $class) {
×
226
            $plugin = $this->getById($id);
×
227
            if ($plugin) {
×
228
                $data[] = $plugin;
×
229
            }
230
        }
231

232
        return $data;
×
233
    }
234

235
    /**
236
     * 根据插件ID获取插件对象
237
     *
238
     * @param string $id
239
     * @return \Miaoxing\Plugin\BasePlugin
240
     * @throws Exception 当插件类不存在时
241
     */
242
    public function getOneById($id)
243
    {
244
        $plugin = $this->getById($id);
9✔
245
        if (!$plugin) {
9✔
246
            throw new Exception(sprintf('Plugin "%s" not found', $id));
3✔
247
        }
248

249
        return $plugin;
6✔
250
    }
251

252
    /**
253
     * 根据插件ID获取插件对象
254
     *
255
     * @param string $id
256
     * @return false|\Miaoxing\Plugin\BasePlugin
257
     */
258
    public function getById($id)
259
    {
260
        if (!isset($this->pluginInstances[$id])) {
15✔
261
            $class = $this->getPluginClass($id);
3✔
262
            if (!class_exists($class)) {
3✔
263
                $this->pluginInstances[$id] = false;
3✔
264
            } else {
265
                $this->pluginInstances[$id] = new $class(['wei' => $this->wei]);
×
266
            }
267
        }
268

269
        return $this->pluginInstances[$id];
15✔
270
    }
271

272
    /**
273
     * Install a plugin by ID
274
     *
275
     * @param string $id
276
     * @return Ret
277
     */
278
    public function install($id)
279
    {
280
        $plugin = $this->getById($id);
×
281
        if (!$plugin) {
×
282
            return err('插件不存在');
×
283
        }
284

NEW
285
        $installedIds = $this->getInstalledIds();
×
NEW
286
        $toInstallIds = array_merge($plugin->getDepIds(), [$id]);
×
287

NEW
288
        $rets = [];
×
NEW
289
        foreach ($toInstallIds as $pluginId) {
×
NEW
290
            if (in_array($pluginId, $installedIds)) {
×
NEW
291
                $rets[] = err(['插件 %s 已安装过', $pluginId]);
×
NEW
292
                continue;
×
293
            }
294

NEW
295
            $plugin = $this->getById($pluginId);
×
NEW
296
            $ret = $plugin->install();
×
NEW
297
            if ($ret->isSuc()) {
×
NEW
298
                $rets[] = suc(['插件 %s 安装成功', $pluginId]);
×
NEW
299
                continue;
×
300
            }
301

NEW
302
            $ret['rets'] = $rets;
×
UNCOV
303
            return $ret;
×
304
        }
305

NEW
306
        $this->setInstalledIds(array_merge($installedIds, $toInstallIds));
×
307

308
        $this->getEvents(true);
×
309

NEW
310
        return suc(['rets' => $rets]);
×
311
    }
312

313
    /**
314
     * Uninstall a plugin by ID
315
     *
316
     * @param string $id
317
     * @return Ret
318
     */
319
    public function uninstall($id)
320
    {
321
        $plugin = $this->getById($id);
×
322
        if (!$plugin) {
×
323
            return err('插件不存在');
×
324
        }
325

326
        if (!$this->isInstalled($id)) {
×
327
            return err('插件未安装');
×
328
        }
329

330
        if ($this->isBuildIn($id)) {
×
331
            return err('不能卸载内置插件');
×
332
        }
333

334
        $ret = $plugin->uninstall();
×
335
        if ($ret->isErr()) {
×
336
            return $ret;
×
337
        }
338

339
        $pluginIds = $this->getInstalledIds();
×
340
        $key = array_search($id, $pluginIds, true);
×
341
        if (false === $key) {
×
342
            return err('插件未安装');
×
343
        }
344
        unset($pluginIds[$key]);
×
345
        $this->setInstalledIds($pluginIds);
×
346

347
        $this->getEvents(true);
×
348

349
        return $ret;
×
350
    }
351

352
    /**
353
     * Check if a plugin is build in
354
     *
355
     * @param string $id
356
     * @return bool
357
     */
358
    public function isBuildIn($id)
359
    {
360
        return in_array($id, $this->builtIns, true);
×
361
    }
362

363
    /**
364
     * 获取所有已安装插件的事件
365
     *
366
     * @param bool $fresh 是否刷新缓存,获得最新配置
367
     * @return array
368
     */
369
    public function getEvents($fresh = false)
370
    {
371
        if (!$this->events || true == $fresh) {
57✔
372
            $cacheKey = 'plugin-events-' . $this->app->getId();
57✔
373

374
            // 清除已有缓存
375
            if ($fresh || $this->isRefresh()) {
57✔
376
                $this->cache->delete($cacheKey);
×
377
            }
378

379
            $this->events = $this->cache->remember($cacheKey, function () {
38✔
380
                $events = [];
×
381
                foreach ($this->getAll() as $plugin) {
×
382
                    $id = $plugin->getId();
×
383
                    if (!$this->isInstalled($id)) {
×
384
                        continue;
×
385
                    }
386
                    foreach ($this->getEventsById($id) as $event) {
×
387
                        $events[$event['name']][$event['priority']][] = $id;
×
388
                    }
389
                }
390
                ksort($events);
×
391
                return $events;
×
392
            });
57✔
393
        }
394

395
        return $this->events;
57✔
396
    }
397

398
    /**
399
     * Load plugin event by name
400
     *
401
     * @param string $name
402
     */
403
    public function loadEvent($name)
404
    {
405
        // 1. Load event data only once
406
        if (isset($this->loadedEvents[$name])) {
507✔
407
            return;
498✔
408
        }
409
        $this->loadedEvents[$name] = true;
57✔
410

411
        // 2. Get event handlers
412
        $events = $this->getEvents();
57✔
413
        if (!isset($events[$name])) {
57✔
414
            return;
57✔
415
        }
416

417
        // 3. Attach handlers to event
418
        $baseMethod = 'on' . ucfirst($name);
×
419
        foreach ($events[$name] as $priority => $pluginIds) {
×
420
            if ($priority && $priority != static::DEFAULT_PRIORITY) {
×
421
                $method = $baseMethod . $priority;
×
422
            } else {
423
                $method = $baseMethod;
×
424
            }
425

426
            foreach ($pluginIds as $pluginId) {
×
427
                $plugin = $this->getById($pluginId);
×
428
                if (method_exists($plugin, $method)) {
×
429
                    $this->event->on($name, [$plugin, $method], $priority);
×
430
                }
431
            }
432
        }
433
    }
434

435
    public function getPluginIdByClass($class)
436
    {
437
        // 类名如:Miaoxing\App\Controller\Apps
438
        $id = explode('\\', $class, 3)[1];
×
439
        $id = $this->dash($id);
×
440

441
        return $id;
×
442
    }
443

444
    /**
445
     * @return array
446
     */
447
    public function getBasePaths()
448
    {
449
        return $this->basePaths;
×
450
    }
451

452
    /**
453
     * Load service configs
454
     *
455
     * @param bool $refresh
456
     * @return $this
457
     * @svc
458
     */
459
    protected function loadConfig($refresh = false)
460
    {
461
        // Load configs to services
462
        $config = $this->getConfig($refresh);
3✔
463
        $this->wei->setConfig($config + [
3✔
464
                'event' => [
2✔
465
                    'loadEvent' => [$this, 'loadEvent'],
3✔
466
                ],
2✔
467
                'view' => [
2✔
468
                    'parseResource' => [$this, 'parseViewResource'],
3✔
469
                ],
2✔
470
            ]);
2✔
471
        return $this;
3✔
472
    }
473

474
    /**
475
     * Get services defined in plugins
476
     *
477
     * @return array
478
     */
479
    protected function getWeiAliases()
480
    {
481
        return array_diff_key(
×
482
            $this->classMap->generate($this->basePaths, '/Service/*.php', 'Service'),
×
483
            array_flip($this->ignoredServices)
×
484
        );
485
    }
486

487
    /**
488
     * Get preload defined in composer.json
489
     *
490
     * @return array
491
     */
492
    protected function getWeiPreload()
493
    {
494
        $preload = [];
×
495
        $files = glob('plugins/*/composer.json');
×
496
        foreach ($files as $file) {
×
497
            $config = json_decode(file_get_contents($file), true);
×
498
            if (isset($config['extra']['wei-preload'])) {
×
499
                $preload = array_merge($preload, $config['extra']['wei-preload']);
×
500
            }
501
        }
502
        return $preload;
×
503
    }
504

505
    /**
506
     * Get all plugin classes
507
     *
508
     * @param bool $refresh
509
     * @return array
510
     * @throws Exception
511
     */
512
    protected function getPluginClasses($refresh = false)
513
    {
514
        if ($refresh || !$this->pluginClasses) {
×
515
            $this->pluginClasses = [];
×
516
            $classes = $this->classMap->generate($this->basePaths, '/*Plugin.php', '', false, true);
×
517
            foreach ($classes as $class) {
×
518
                $parts = explode('\\', $class);
×
519
                $name = end($parts);
×
520
                // Remove "Plugin" suffix
521
                $name = substr($name, 0, -6);
×
522
                $name = $this->dash($name);
×
523
                $this->pluginClasses[$name] = $class;
×
524
            }
525
        }
526

527
        return $this->pluginClasses;
×
528
    }
529

530
    /**
531
     * 判断请求是否要求刷新缓存
532
     *
533
     * @return bool
534
     */
535
    protected function isRefresh()
536
    {
537
        return $this->wei->isDebug()
60✔
538
            && 'no-cache' == $this->req->getServer('HTTP_PRAGMA')
60✔
539
            && false === strpos($this->req->getServer('HTTP_USER_AGENT'), 'wechatdevtools');
60✔
540
    }
541

542
    /**
543
     * 执行指定的回调,并存储到缓存中
544
     *
545
     * @param string $key
546
     * @param bool $refresh
547
     * @param callable $fn
548
     * @return mixed
549
     */
550
    protected function getCache($key, $refresh, callable $fn)
551
    {
552
        if ($refresh || $this->isRefresh()) {
3✔
553
            $this->configCache->delete($key);
×
554
        }
555

556
        return $this->configCache->remember($key, function () use ($fn) {
2✔
557
            return $fn();
×
558
        });
3✔
559
    }
560

561
    /**
562
     * Check if a plugin exists
563
     *
564
     * @param string $id
565
     * @return bool
566
     * @svc
567
     */
568
    protected function has($id)
569
    {
570
        return class_exists($this->getPluginClass($id));
×
571
    }
572

573
    /**
574
     * Check if a plugin is installed
575
     *
576
     * @param string $id
577
     * @return bool
578
     * @svc
579
     */
580
    protected function isInstalled($id)
581
    {
582
        return $this->isBuildIn($id) || in_array($id, $this->getInstalledIds(), true);
×
583
    }
584

585
    /**
586
     * Returns the plugin class by plugin ID
587
     *
588
     * @param string $id
589
     * @return string
590
     */
591
    protected function getPluginClass($id)
592
    {
593
        return isset($this->pluginClasses[$id]) ? $this->pluginClasses[$id] : null;
3✔
594
    }
595

596
    /**
597
     * Returns the event definitions by plugin ID
598
     *
599
     * @param string $id
600
     * @return array
601
     */
602
    protected function getEventsById($id)
603
    {
604
        $events = [];
×
605
        $methods = get_class_methods($this->getPluginClass($id));
×
606
        foreach ($methods as $method) {
×
607
            // The event naming is onName[Priority],eg onProductShowItem50
608
            if ('on' != substr($method, 0, 2)) {
×
609
                continue;
×
610
            }
611
            $event = lcfirst(substr($method, 2));
×
612
            if (is_numeric(substr($event, -1))) {
×
613
                preg_match('/(.+?)(\d+)$/', $event, $matches);
×
614
                $events[] = ['name' => $matches[1], 'priority' => (int) $matches[2]];
×
615
            } else {
616
                $events[] = ['name' => $event, 'priority' => static::DEFAULT_PRIORITY];
×
617
            }
618
        }
619

620
        return $events;
×
621
    }
622

623
    /**
624
     * @param string $name
625
     * @return string
626
     */
627
    protected function dash($name)
628
    {
629
        return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $name));
×
630
    }
631

632
    /**
633
     * @param string $class
634
     * @return bool
635
     */
636
    protected function autoload($class)
637
    {
638
        if (0 !== strpos($class, 'Miaoxing\\')) {
483✔
639
            return false;
483✔
640
        }
641

642
        // Ignore prefix namespace
643
        [$ignore, $name, $path] = explode('\\', $class, 3);
×
644

645
        $ds = \DIRECTORY_SEPARATOR;
×
646
        $file = implode($ds, ['plugins', $this->dash($name), 'src', strtr($path, ['\\' => $ds])]) . '.php';
×
647
        if (file_exists($file)) {
×
648
            require_once $file;
×
649
            return true;
×
650
        }
651

652
        return false;
×
653
    }
654

655
    /**
656
     * Returns installed plugin IDs
657
     *
658
     * @return array
659
     */
660
    protected function getInstalledIds()
661
    {
662
        return (array) $this->app->getModel()->get('pluginIds');
×
663
    }
664

665
    /**
666
     * Stores installed plugin IDs
667
     *
668
     * @param array $pluginIds
669
     * @return $this
670
     */
671
    protected function setInstalledIds(array $pluginIds)
672
    {
673
        $app = $this->app->getModel();
×
NEW
674
        $app['pluginIds'] = array_filter(array_unique($pluginIds));
×
675
        $app->save();
×
676

677
        return $this;
×
678
    }
679
}
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