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

miaoxing / plugin / 6057194056

02 Sep 2023 07:53AM UTC coverage: 39.283% (+0.2%) from 39.08%
6057194056

push

github

semantic-release-bot
chore(release): publish

See CHANGELOG.md for more details.

920 of 2342 relevant lines covered (39.28%)

18.04 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

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

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

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

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

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

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

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

106
        parent::__construct($options);
3✔
107

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

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

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

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

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

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

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

189
        if ($pluginId) {
6✔
190
            $plugin = $this->getOneById($pluginId);
3✔
191
            $path = $plugin->getBasePath() . '/views';
3✔
192

193
            return ['path' => $path, 'file' => $file];
3✔
194
        } else {
195
            return ['path' => null, 'file' => $resource];
6✔
196
        }
197
    }
198

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

212
        return $components;
6✔
213
    }
214

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

230
        return $data;
×
231
    }
232

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

247
        return $plugin;
6✔
248
    }
249

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

267
        return $this->pluginInstances[$id];
15✔
268
    }
269

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

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

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

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

300
            $ret['rets'] = $rets;
×
301
            return $ret;
×
302
        }
303

304
        $this->setInstalledIds(array_merge($installedIds, $toInstallIds));
×
305

306
        $this->getEvents(true);
×
307

308
        return suc(['rets' => $rets]);
×
309
    }
310

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

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

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

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

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

345
        $this->getEvents(true);
×
346

347
        return $ret;
×
348
    }
349

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

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

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

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

393
        return $this->events;
57✔
394
    }
395

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

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

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

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

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

439
        return $id;
×
440
    }
441

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

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

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

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

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

525
        return $this->pluginClasses;
×
526
    }
527

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

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

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

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

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

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

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

618
        return $events;
×
619
    }
620

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

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

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

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

650
        return false;
×
651
    }
652

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

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

675
        return $this;
×
676
    }
677
}
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