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

miaoxing / plugin / 4055264541

pending completion
4055264541

push

github

semantic-release-bot
chore(release): publish

870 of 2273 relevant lines covered (38.28%)

19.01 hits per line

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

32.2
/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

285
        if ($this->isInstalled($id)) {
×
286
            return err('插件已安装');
×
287
        }
288

289
        $ret = $plugin->install();
×
290
        if ($ret->isErr()) {
×
291
            return $ret;
×
292
        }
293

294
        $pluginIds = $this->getInstalledIds();
×
295
        $pluginIds[] = $id;
×
296
        $this->setInstalledIds($pluginIds);
×
297

298
        $this->getEvents(true);
×
299

300
        return $ret;
×
301
    }
302

303
    /**
304
     * Uninstall a plugin by ID
305
     *
306
     * @param string $id
307
     * @return Ret
308
     */
309
    public function uninstall($id)
310
    {
311
        $plugin = $this->getById($id);
×
312
        if (!$plugin) {
×
313
            return err('插件不存在');
×
314
        }
315

316
        if (!$this->isInstalled($id)) {
×
317
            return err('插件未安装');
×
318
        }
319

320
        if ($this->isBuildIn($id)) {
×
321
            return err('不能卸载内置插件');
×
322
        }
323

324
        $ret = $plugin->uninstall();
×
325
        if ($ret->isErr()) {
×
326
            return $ret;
×
327
        }
328

329
        $pluginIds = $this->getInstalledIds();
×
330
        $key = array_search($id, $pluginIds, true);
×
331
        if (false === $key) {
×
332
            return err('插件未安装');
×
333
        }
334
        unset($pluginIds[$key]);
×
335
        $this->setInstalledIds($pluginIds);
×
336

337
        $this->getEvents(true);
×
338

339
        return $ret;
×
340
    }
341

342
    /**
343
     * Check if a plugin is build in
344
     *
345
     * @param string $id
346
     * @return bool
347
     */
348
    public function isBuildIn($id)
349
    {
350
        return in_array($id, $this->builtIns, true);
×
351
    }
352

353
    /**
354
     * 获取所有已安装插件的事件
355
     *
356
     * @param bool $fresh 是否刷新缓存,获得最新配置
357
     * @return array
358
     */
359
    public function getEvents($fresh = false)
360
    {
361
        if (!$this->events || true == $fresh) {
60✔
362
            $cacheKey = 'plugin-events-' . $this->app->getId();
60✔
363

364
            // 清除已有缓存
365
            if ($fresh || $this->isRefresh()) {
60✔
366
                $this->cache->delete($cacheKey);
×
367
            }
368

369
            $this->events = $this->cache->remember($cacheKey, function () {
40✔
370
                $events = [];
×
371
                foreach ($this->getAll() as $plugin) {
×
372
                    $id = $plugin->getId();
×
373
                    if (!$this->isInstalled($id)) {
×
374
                        continue;
×
375
                    }
376
                    foreach ($this->getEventsById($id) as $event) {
×
377
                        $events[$event['name']][$event['priority']][] = $id;
×
378
                    }
379
                }
380
                ksort($events);
×
381
                return $events;
×
382
            });
60✔
383
        }
384

385
        return $this->events;
60✔
386
    }
387

388
    /**
389
     * Load plugin event by name
390
     *
391
     * @param string $name
392
     */
393
    public function loadEvent($name)
394
    {
395
        // 1. Load event data only once
396
        if (isset($this->loadedEvents[$name])) {
519✔
397
            return;
507✔
398
        }
399
        $this->loadedEvents[$name] = true;
60✔
400

401
        // 2. Get event handlers
402
        $events = $this->getEvents();
60✔
403
        if (!isset($events[$name])) {
60✔
404
            return;
60✔
405
        }
406

407
        // 3. Attach handlers to event
408
        $baseMethod = 'on' . ucfirst($name);
×
409
        foreach ($events[$name] as $priority => $pluginIds) {
×
410
            if ($priority && $priority != static::DEFAULT_PRIORITY) {
×
411
                $method = $baseMethod . $priority;
×
412
            } else {
413
                $method = $baseMethod;
×
414
            }
415

416
            foreach ($pluginIds as $pluginId) {
×
417
                $plugin = $this->getById($pluginId);
×
418
                if (method_exists($plugin, $method)) {
×
419
                    $this->event->on($name, [$plugin, $method], $priority);
×
420
                }
421
            }
422
        }
423
    }
424

425
    public function getPluginIdByClass($class)
426
    {
427
        // 类名如:Miaoxing\App\Controller\Apps
428
        $id = explode('\\', $class, 3)[1];
×
429
        $id = $this->dash($id);
×
430

431
        return $id;
×
432
    }
433

434
    /**
435
     * @return array
436
     */
437
    public function getBasePaths()
438
    {
439
        return $this->basePaths;
×
440
    }
441

442
    /**
443
     * Load service configs
444
     *
445
     * @param bool $refresh
446
     * @return $this
447
     * @svc
448
     */
449
    protected function loadConfig($refresh = false)
450
    {
451
        // Load configs to services
452
        $config = $this->getConfig($refresh);
3✔
453
        $this->wei->setConfig($config + [
3✔
454
                'event' => [
2✔
455
                    'loadEvent' => [$this, 'loadEvent'],
3✔
456
                ],
2✔
457
                'view' => [
2✔
458
                    'parseResource' => [$this, 'parseViewResource'],
3✔
459
                ],
2✔
460
            ]);
2✔
461
        return $this;
3✔
462
    }
463

464
    /**
465
     * Get services defined in plugins
466
     *
467
     * @return array
468
     */
469
    protected function getWeiAliases()
470
    {
471
        return array_diff_key(
×
472
            $this->classMap->generate($this->basePaths, '/Service/*.php', 'Service'),
×
473
            array_flip($this->ignoredServices)
×
474
        );
475
    }
476

477
    /**
478
     * Get preload defined in composer.json
479
     *
480
     * @return array
481
     */
482
    protected function getWeiPreload()
483
    {
484
        $preload = [];
×
485
        $files = glob('plugins/*/composer.json');
×
486
        foreach ($files as $file) {
×
487
            $config = json_decode(file_get_contents($file), true);
×
488
            if (isset($config['extra']['wei-preload'])) {
×
489
                $preload = array_merge($preload, $config['extra']['wei-preload']);
×
490
            }
491
        }
492
        return $preload;
×
493
    }
494

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

517
        return $this->pluginClasses;
×
518
    }
519

520
    /**
521
     * 判断请求是否要求刷新缓存
522
     *
523
     * @return bool
524
     */
525
    protected function isRefresh()
526
    {
527
        return $this->wei->isDebug()
63✔
528
            && 'no-cache' == $this->req->getServer('HTTP_PRAGMA')
63✔
529
            && false === strpos($this->req->getServer('HTTP_USER_AGENT'), 'wechatdevtools');
63✔
530
    }
531

532
    /**
533
     * 执行指定的回调,并存储到缓存中
534
     *
535
     * @param string $key
536
     * @param bool $refresh
537
     * @param callable $fn
538
     * @return mixed
539
     */
540
    protected function getCache($key, $refresh, callable $fn)
541
    {
542
        if ($refresh || $this->isRefresh()) {
3✔
543
            $this->configCache->delete($key);
×
544
        }
545

546
        return $this->configCache->remember($key, function () use ($fn) {
2✔
547
            return $fn();
×
548
        });
3✔
549
    }
550

551
    /**
552
     * Check if a plugin exists
553
     *
554
     * @param string $id
555
     * @return bool
556
     * @svc
557
     */
558
    protected function has($id)
559
    {
560
        return class_exists($this->getPluginClass($id));
×
561
    }
562

563
    /**
564
     * Check if a plugin is installed
565
     *
566
     * @param string $id
567
     * @return bool
568
     * @svc
569
     */
570
    protected function isInstalled($id)
571
    {
572
        return $this->isBuildIn($id) || in_array($id, $this->getInstalledIds(), true);
×
573
    }
574

575
    /**
576
     * Returns the plugin class by plugin ID
577
     *
578
     * @param string $id
579
     * @return string
580
     */
581
    protected function getPluginClass($id)
582
    {
583
        return isset($this->pluginClasses[$id]) ? $this->pluginClasses[$id] : null;
3✔
584
    }
585

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

610
        return $events;
×
611
    }
612

613
    /**
614
     * @param string $name
615
     * @return string
616
     */
617
    protected function dash($name)
618
    {
619
        return strtolower(preg_replace('~(?<=\\w)([A-Z])~', '-$1', $name));
×
620
    }
621

622
    /**
623
     * @param string $class
624
     * @return bool
625
     */
626
    protected function autoload($class)
627
    {
628
        if (0 !== strpos($class, 'Miaoxing\\')) {
495✔
629
            return false;
495✔
630
        }
631

632
        // Ignore prefix namespace
633
        [$ignore, $name, $path] = explode('\\', $class, 3);
×
634

635
        $ds = \DIRECTORY_SEPARATOR;
×
636
        $file = implode($ds, ['plugins', $this->dash($name), 'src', strtr($path, ['\\' => $ds])]) . '.php';
×
637
        if (file_exists($file)) {
×
638
            require_once $file;
×
639
            return true;
×
640
        }
641

642
        return false;
×
643
    }
644

645
    /**
646
     * Returns installed plugin IDs
647
     *
648
     * @return array
649
     */
650
    protected function getInstalledIds()
651
    {
652
        return (array) $this->app->getModel()->get('pluginIds');
×
653
    }
654

655
    /**
656
     * Stores installed plugin IDs
657
     *
658
     * @param array $pluginIds
659
     * @return $this
660
     */
661
    protected function setInstalledIds(array $pluginIds)
662
    {
663
        $app = $this->app->getModel();
×
664
        $app['pluginIds'] = array_filter($pluginIds);
×
665
        $app->save();
×
666

667
        return $this;
×
668
    }
669
}
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