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

miaoxing / plugin / 5284002252

pending completion
5284002252

push

github

semantic-release-bot
chore(release): publish

See CHANGELOG.md for more details.

855 of 2246 relevant lines covered (38.07%)

18.59 hits per line

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

97.66
/src/Service/Config.php
1
<?php
2

3
namespace Miaoxing\Plugin\Service;
4

5
use Miaoxing\Plugin\Model\ModelTrait;
6
use Wei\Base;
7

8
/**
9
 * @mixin \EnvMixin
10
 * @mixin \CacheMixin
11
 * @mixin \AppMixin
12
 * @mixin \PhpFileCacheMixin
13
 */
14
class Config extends \Wei\Config
15
{
16
    /**
17
     * @internal
18
     */
19
    protected const TYPE_STRING = 's';
20

21
    /**
22
     * @internal
23
     */
24
    protected const TYPE_BOOL = 'b';
25

26
    /**
27
     * @internal
28
     */
29
    protected const TYPE_INT = 'i';
30

31
    /**
32
     * @internal
33
     */
34
    protected const TYPE_FLOAT = 'f';
35

36
    /**
37
     * @internal
38
     */
39
    protected const TYPE_NULL = 'n';
40

41
    /**
42
     * @internal
43
     */
44
    protected const TYPE_ARRAY = 'a';
45

46
    /**
47
     * @internal
48
     */
49
    protected const TYPE_OBJECT = 'o';
50

51
    /**
52
     * @internal
53
     */
54
    protected const TYPE_JSON = 'j';
55

56
    /**
57
     * @internal
58
     */
59
    protected const TYPE_EXPRESS = 'e';
60

61
    /**
62
     * The local config file path
63
     *
64
     * @var string
65
     */
66
    protected $localFile = 'storage/configs/%env%.php';
67

68
    /**
69
     * @var array
70
     */
71
    protected $services = [];
72

73
    /**
74
     * The missing config names of the last get action
75
     *
76
     * @var array<string>
77
     */
78
    protected $missing = [];
79

80
    /**
81
     * Native scalar type to config type
82
     *
83
     * @var string[]
84
     * @internal
85
     */
86
    protected $scalarTypes = [
87
        'boolean' => self::TYPE_BOOL,
88
        'integer' => self::TYPE_INT,
89
        'double' => self::TYPE_FLOAT,
90
        'string' => self::TYPE_STRING,
91
        'NULL' => self::TYPE_NULL,
92
        // ignore non-scalar types
93
    ];
94

95
    /**
96
     * @svc
97
     * @param mixed $default
98
     */
99
    protected function get(string $name, $default = null)
100
    {
101
        return $this->getMultiple([$name], [$name => $default])[$name];
28✔
102
    }
103

104
    /**
105
     * @svc
106
     * @param mixed $value
107
     */
108
    protected function set(string $name, $value, array $options = []): self
109
    {
110
        return $this->setApp($name, $value, $options);
8✔
111
    }
112

113
    /**
114
     * Get multiple configs
115
     *
116
     * @param array $names The names of config
117
     * @param array $defaults The values to return when config not found or is null
118
     * @return array
119
     * @svc
120
     */
121
    protected function getMultiple(array $names, array $defaults = []): array
122
    {
123
        $values = $this->getAppMultiple($names);
44✔
124
        $nulls = $this->getNullKeys($values);
44✔
125

126
        if ($nulls) {
44✔
127
            $globalValues = $this->getGlobalMultiple($nulls);
28✔
128
            $values = array_merge($values, $globalValues);
28✔
129
            $nulls = $this->getNullKeys($globalValues);
28✔
130
        }
131

132
        foreach ($nulls as $name) {
44✔
133
            // 注意: 配置名称不含 . 时,文件配置会返回所有下级数据
134
            // 行为和数据库存储配置不一致,但一般不影响使用
135
            $values[$name] = $this->wei->getConfig($name);
20✔
136
        }
137

138
        foreach ($names as $name) {
44✔
139
            $values[$name] = $values[$name] ?? $defaults[$name] ?? null;
44✔
140
        }
141

142
        return $values;
44✔
143
    }
144

145
    /**
146
     * @svc
147
     */
148
    protected function setMultiple(array $values, array $options = []): self
149
    {
150
        return $this->setAppMultiple($values, $options);
8✔
151
    }
152

153
    /**
154
     * @svc
155
     */
156
    protected function getSection(string $name): array
157
    {
158
        return array_merge(
20✔
159
            (array) $this->wei->getConfig($name),
20✔
160
            $this->getGlobalSection($name),
20✔
161
            $this->getAppSection($name)
20✔
162
        );
10✔
163
    }
164

165
    /**
166
     * @svc
167
     * @param mixed $default
168
     */
169
    protected function getGlobal(string $name, $default = null)
170
    {
171
        return $this->getMultipleBy(GlobalConfigModel::class, [$name], [$name => $default])[$name];
64✔
172
    }
173

174
    /**
175
     * @svc
176
     * @param mixed $value
177
     */
178
    protected function setGlobal(string $name, $value, array $options = []): self
179
    {
180
        return $this->setMultipleBy(GlobalConfigModel::class, [$name => $value], $options);
80✔
181
    }
182

183
    /**
184
     * @svc
185
     */
186
    protected function deleteGlobal(string $name): self
187
    {
188
        return $this->deleteBy(GlobalConfigModel::class, $name);
24✔
189
    }
190

191
    /**
192
     * @svc
193
     */
194
    protected function getGlobalMultiple(array $names, array $defaults = []): array
195
    {
196
        return $this->getMultipleBy(GlobalConfigModel::class, $names, $defaults);
36✔
197
    }
198

199
    /**
200
     * @svc
201
     */
202
    protected function setGlobalMultiple(array $values, array $options = []): self
203
    {
204
        return $this->setMultipleBy(GlobalConfigModel::class, $values, $options);
12✔
205
    }
206

207
    /**
208
     * @svc
209
     */
210
    protected function getGlobalSection(string $name): array
211
    {
212
        return $this->getSectionBy(GlobalConfigModel::class, $name);
24✔
213
    }
214

215
    /**
216
     * @svc
217
     * @param mixed $default
218
     */
219
    protected function getApp(string $name, $default = null)
220
    {
221
        return $this->getMultipleBy(ConfigModel::class, [$name], [$name => $default])[$name];
32✔
222
    }
223

224
    /**
225
     * @svc
226
     * @param mixed $value
227
     */
228
    protected function setApp(string $name, $value, array $options = []): self
229
    {
230
        return $this->setMultipleBy(ConfigModel::class, [$name => $value], $options);
40✔
231
    }
232

233
    /**
234
     * @svc
235
     */
236
    protected function deleteApp(string $name): self
237
    {
238
        return $this->deleteBy(ConfigModel::class, $name);
28✔
239
    }
240

241
    /**
242
     * @svc
243
     */
244
    protected function getAppMultiple(array $names, array $defaults = []): array
245
    {
246
        return $this->getMultipleBy(ConfigModel::class, $names, $defaults);
52✔
247
    }
248

249
    /**
250
     * @svc
251
     */
252
    protected function setAppMultiple(array $values, array $options = []): self
253
    {
254
        return $this->setMultipleBy(ConfigModel::class, $values, $options);
32✔
255
    }
256

257
    /**
258
     * @svc
259
     */
260
    protected function getAppSection(string $name): array
261
    {
262
        return $this->getSectionBy(ConfigModel::class, $name);
24✔
263
    }
264

265
    /**
266
     * @template T of Base
267
     * @phpstan-ignore-next-line [bleedingEdge]Template type T xxx is not referenced in a parameter. phpstan#5175
268
     * @param string|class-string<T> $name
269
     * @return Base|T
270
     * @svc
271
     */
272
    protected function createService(string $name): Base
273
    {
274
        $name = $this->wei->getServiceName($name);
8✔
275
        $options = $this->getSection($name);
8✔
276
        return $this->wei->newInstance($name, $options);
8✔
277
    }
278

279
    /**
280
     * @template T of Base
281
     * @phpstan-ignore-next-line [bleedingEdge]Template type T xxx is not referenced in a parameter. phpstan#5175
282
     * @param string|class-string<T> $name
283
     * @return Base|T
284
     * @svc
285
     */
286
    protected function getService(string $name): Base
287
    {
288
        $appId = $this->app->getId();
8✔
289
        $name = $this->wei->getServiceName($name);
8✔
290
        if (!isset($this->services[$appId][$name])) {
8✔
291
            $this->services[$appId][$name] = $this->createService($name);
4✔
292
        }
293
        return $this->services[$appId][$name];
8✔
294
    }
295

296
    /**
297
     * 预加载全局配置
298
     *
299
     * @experimental
300
     * @svc
301
     */
302
    protected function preloadGlobal()
303
    {
304
        // 1. 先获取本地配置
305
        $configs = $this->getPhpFileCache('global-config', []);
4✔
306

307
        // 2. 检查更新配置
308
        if ($this->needsUpdatePreload($configs)) {
4✔
309
            $configs = $this->setPreloadCache();
310
        }
311

312
        // 3. 加载配置
313
        $this->wei->setConfig($configs);
4✔
314
    }
2✔
315

316
    /**
317
     * @return $this
318
     * @svc
319
     */
320
    protected function publishPreload(): self
321
    {
322
        $this->updatePreloadVersion();
4✔
323
        $configs = $this->setPreloadCache();
4✔
324
        $this->wei->setConfig($configs);
4✔
325
        return $this;
4✔
326
    }
327

328
    /**
329
     * @return string
330
     * @internal
331
     */
332
    protected function updatePreloadVersion(): string
333
    {
334
        $version = date('Y-m-d H:i:s');
8✔
335
        $this->setGlobal($this->getPreloadVersionKey(), $version, ['preload' => true]);
8✔
336
        return $version;
8✔
337
    }
338

339
    /**
340
     * @param string|class-string<ModelTrait> $model
341
     * @param array $names
342
     * @param array $defaults
343
     * @return array
344
     */
345
    protected function getMultipleBy(string $model, array $names, array $defaults): array
346
    {
347
        $this->missing = [];
148✔
348
        $prefix = $this->getPrefix($model);
148✔
349

350
        // From cache
351
        [$values, $missing] = $this->getMultipleFromCache($prefix, $names);
148✔
352

353
        // From database
354
        if ($missing) {
148✔
355
            [$dbValues, $missing] = $this->getMultipleFromDb($model, $names);
136✔
356
            $values = array_merge($values, $dbValues);
136✔
357

358
            // Next time will fetch from cache
359
            $cacheValues = $dbValues;
136✔
360
            foreach ($missing as $name) {
136✔
361
                $cacheValues[$name] = null;
72✔
362
            }
363
            $this->cacheWithPrefix($prefix, function () use ($cacheValues) {
68✔
364
                $this->cache->setMultiple($cacheValues);
136✔
365
            });
136✔
366
        }
367
        $this->missing = $missing;
148✔
368

369
        // Add default value
370
        foreach ($values as $name => $value) {
148✔
371
            $values[$name] = $value ?? $defaults[$name] ?? null;
148✔
372
        }
373

374
        return $values;
148✔
375
    }
376

377
    /**
378
     * @param string $prefix
379
     * @param array $names
380
     * @return array
381
     * @internal
382
     */
383
    protected function getMultipleFromCache(string $prefix, array $names): array
384
    {
385
        // From cache
386
        if (1 === count($names)) {
148✔
387
            $key = current($names);
120✔
388
            $values = [$key => $this->cache->get($prefix . $key)];
120✔
389
        } else {
390
            $values = $this->cacheWithPrefix($prefix, function () use ($names) {
16✔
391
                return $this->cache->getMultiple($names);
32✔
392
            });
32✔
393
        }
394

395
        $missing = [];
148✔
396
        foreach ($values as $name => $value) {
148✔
397
            if (!$this->cache->isHit($prefix . $name)) {
148✔
398
                $missing[] = $name;
136✔
399
            }
400
        }
401

402
        return [$values, $missing];
148✔
403
    }
404

405
    /**
406
     * @param string|class-string<ModelTrait> $model
407
     * @param array $names
408
     * @return array
409
     * @internal
410
     */
411
    protected function getMultipleFromDb(string $model, array $names): array
412
    {
413
        $configs = $model::select(['name', 'type', 'value'])
132✔
414
            ->where('name', $names)
132✔
415
            ->fetchAll();
132✔
416

417
        $values = [];
132✔
418
        foreach ($configs as $config) {
132✔
419
            $values[$config['name']] = $this->decode($config['value'], $config['type']);
90✔
420
        }
421

422
        $missing = [];
132✔
423
        foreach ($names as $name) {
132✔
424
            if (!array_key_exists($name, $values)) {
132✔
425
                $missing[] = $name;
68✔
426
            }
427
        }
428

429
        return [$values, $missing];
132✔
430
    }
431

432
    /**
433
     * @param string|class-string<ModelTrait> $model
434
     * @param array $values
435
     * @param array $options
436
     * @return $this
437
     */
438
    protected function setMultipleBy(string $model, array $values, array $options = []): self
439
    {
440
        if (!$values) {
144✔
441
            return $this;
4✔
442
        }
443

444
        $configs = $model::where('name', array_keys($values))
140✔
445
            ->indexBy('name')
140✔
446
            ->all();
140✔
447

448
        foreach ($values as $name => $value) {
140✔
449
            if (!isset($configs[$name])) {
140✔
450
                $configs[$name] = $model::fromArray(['name' => $name]);
64✔
451
            }
452

453
            [$value, $type] = $this->encode($value);
140✔
454
            $configs[$name]->fromArray([
140✔
455
                'type' => $type,
140✔
456
                'value' => $value,
140✔
457
            ]);
70✔
458

459
            // For global config
460
            if (isset($options['preload'])) {
140✔
461
                $configs[$name]->preload = $options['preload'];
12✔
462
            }
463
        }
464

465
        $configs->save();
140✔
466

467
        $data = [];
140✔
468
        $prefix = $this->getPrefix($model);
140✔
469
        foreach ($values as $key => $value) {
140✔
470
            $data[$prefix . $key] = $value;
140✔
471
        }
472
        $this->cache->setMultiple($data);
140✔
473

474
        return $this;
140✔
475
    }
476

477
    /**
478
     * @param string|class-string<ModelTrait> $model
479
     * @param string $name
480
     * @return $this
481
     * @internal
482
     */
483
    protected function deleteBy(string $model, string $name): self
484
    {
485
        $this->cache->delete($this->getPrefix($model) . $name);
44✔
486

487
        $config = $model::findBy('name', $name);
44✔
488
        $config && $config->destroy();
44✔
489

490
        return $this;
44✔
491
    }
492

493
    /**
494
     * @param string|class-string<ModelTrait> $model
495
     * @param string $name
496
     * @return array
497
     */
498
    protected function getSectionBy(string $model, string $name): array
499
    {
500
        // From database
501
        $configs = $model::select(['name', 'type', 'value'])
28✔
502
            ->where('name', 'like', $name . '.%')
28✔
503
            ->fetchAll();
28✔
504

505
        $length = strlen($name) + 1;
28✔
506
        $values = [];
28✔
507
        foreach ($configs as $config) {
28✔
508
            $values[substr($config['name'], $length)] = $this->decode($config['value'], $config['type']);
28✔
509
        }
510

511
        return $values;
28✔
512
    }
513

514
    /**
515
     * @param string $model
516
     * @return string
517
     */
518
    protected function getPrefix(string $model): string
519
    {
520
        if (ConfigModel::class === $model) {
180✔
521
            return 'config:' . $this->app->getId() . ':';
104✔
522
        }
523
        return 'globalConfig:';
120✔
524
    }
525

526
    /**
527
     * @param string $prefix
528
     * @param callable $fn
529
     * @return mixed
530
     * @internal
531
     */
532
    protected function cacheWithPrefix(string $prefix, callable $fn)
533
    {
534
        $namespace = $this->cache->getNamespace();
136✔
535
        $this->cache->setNamespace($namespace . $prefix);
136✔
536
        $result = $fn();
136✔
537
        $this->cache->setNamespace($namespace);
136✔
538
        return $result;
136✔
539
    }
540

541
    /**
542
     * Convert PHP value to config value
543
     *
544
     * @param mixed $value
545
     * @return array
546
     * @internal
547
     */
548
    protected function encode($value): array
549
    {
550
        $type = gettype($value);
140✔
551
        if (isset($this->scalarTypes[$type])) {
140✔
552
            return [$value, $this->scalarTypes[$type]];
124✔
553
        }
554

555
        if ('array' === $type) {
16✔
556
            return [json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE), static::TYPE_ARRAY];
8✔
557
        }
558

559
        if ($value instanceof \stdClass) {
8✔
560
            return [json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE), static::TYPE_JSON];
4✔
561
        }
562

563
        return [serialize($value), static::TYPE_OBJECT];
4✔
564
    }
565

566
    /**
567
     * Convert config value to PHP value
568
     *
569
     * @param mixed $value
570
     * @param string $type
571
     * @return mixed
572
     * @internal
573
     */
574
    protected function decode($value, string $type)
575
    {
576
        switch ($type) {
577
            case static::TYPE_STRING:
122✔
578
                return (string) $value;
66✔
579

580
            case static::TYPE_INT:
66✔
581
                return (int) $value;
34✔
582

583
            case static::TYPE_FLOAT:
32✔
584
                return (float) $value;
4✔
585

586
            case static::TYPE_BOOL:
28✔
587
                return filter_var($value, \FILTER_VALIDATE_BOOLEAN);
8✔
588

589
            case static::TYPE_ARRAY:
20✔
590
                return json_decode($value, true);
8✔
591

592
            case static::TYPE_JSON:
12✔
593
                return json_decode($value);
4✔
594

595
            case static::TYPE_OBJECT:
8✔
596
                return unserialize($value);
4✔
597

598
            case static::TYPE_NULL:
4✔
599
                return null;
4✔
600

601
            default:
602
                return $value;
603
        }
604
    }
605

606
    /**
607
     * 获取配置文件
608
     *
609
     * @return string
610
     */
611
    protected function getLocalFile(): string
612
    {
613
        return str_replace('%env%', $this->env->getName(), $this->localFile);
8✔
614
    }
615

616
    /**
617
     * 更新配置到本地文件中
618
     *
619
     * @param array $configs
620
     * @svc
621
     */
622
    protected function updateLocal(array $configs)
623
    {
624
        $file = $this->getLocalFile();
4✔
625
        if (is_file($file)) {
4✔
626
            $localConfigs = require $file;
4✔
627
        } else {
628
            $localConfigs = [];
4✔
629
        }
630

631
        foreach ($configs as $name => $value) {
4✔
632
            if (isset($localConfigs[$name])) {
4✔
633
                $localConfigs[$name] = array_merge($localConfigs[$name], $value);
4✔
634
            } else {
635
                $localConfigs[$name] = $value;
4✔
636
            }
637
        }
638

639
        $this->writeConfig($file, $localConfigs);
4✔
640
        $this->wei->setConfig($configs);
4✔
641
    }
2✔
642

643
    /**
644
     * @param string $file
645
     * @param mixed $configs
646
     */
647
    protected function writeConfig(string $file, $configs): void
648
    {
649
        $content = $this->generateContent($configs);
8✔
650
        file_put_contents($file, $content);
8✔
651
        function_exists('opcache_invalidate') && opcache_invalidate($file);
8✔
652
    }
4✔
653

654
    /**
655
     * 将数据库读出的对象生成文件内容
656
     *
657
     * @param array $configs
658
     * @return string
659
     */
660
    protected function generateContent($configs)
661
    {
662
        return "<?php\n\nreturn " . $this->varExport($configs) . ";\n";
8✔
663
    }
664

665
    /**
666
     * @param mixed $var
667
     * @param string $indent
668
     * @return string
669
     * @link https://stackoverflow.com/questions/24316347/how-to-format-var-export-to-php5-4-array-syntax
670
     */
671
    protected function varExport($var, $indent = '')
672
    {
673
        switch (gettype($var)) {
8✔
674
            case 'string':
8✔
675
                return '\'' . addcslashes($var, "\\\$\\'\r\n\t\v\f") . '\'';
8✔
676
            case 'array':
8✔
677
                $indexed = array_keys($var) === range(0, count($var) - 1);
8✔
678
                $result = [];
8✔
679
                foreach ($var as $key => $value) {
8✔
680
                    $result[] = $indent . '    '
8✔
681
                        . ($indexed ? '' : $this->varExport($key) . ' => ')
8✔
682
                        . $this->varExport($value, "$indent    ");
8✔
683
                }
684

685
                return "[\n" . implode(",\n", $result) . ($result ? ',' : '') . "\n" . $indent . ']';
8✔
686
            case 'boolean':
8✔
687
                return $var ? 'true' : 'false';
4✔
688

689
            case 'NULL':
8✔
690
                return 'null';
4✔
691

692
            case 'object':
8✔
693
                if (isset($var->express)) {
694
                    return $var->express;
695
                }
696
            // no break
697

698
            default:
699
                return var_export($var, true);
8✔
700
        }
701
    }
702

703
    /**
704
     * 判断本地的配置是否需要更改
705
     *
706
     * @param array $configs
707
     * @return bool
708
     * @internal
709
     */
710
    protected function needsUpdatePreload(array $configs): bool
711
    {
712
        $key = $this->getPreloadVersionKey();
4✔
713

714
        $version = $this->getGlobal($key);
4✔
715
        if (!$version) {
4✔
716
            $version = $this->updatePreloadVersion();
4✔
717
        }
718

719
        [$service, $option] = explode('.', $key);
4✔
720
        return !isset($configs[$service][$option]) || $configs[$service][$option] < $version;
4✔
721
    }
722

723
    /**
724
     * @return string
725
     * @svc
726
     */
727
    protected function getPreloadVersionKey(): string
728
    {
729
        return 'config.preloadVersion';
8✔
730
    }
731

732
    /**
733
     * @return array
734
     * @internal
735
     */
736
    protected function setPreloadCache(): array
737
    {
738
        $configs = GlobalConfigModel::select('name', 'type', 'value')
4✔
739
            ->where('preload', true)
4✔
740
            ->fetchAll();
4✔
741

742
        $data = [];
4✔
743
        foreach ($configs as $config) {
4✔
744
            // 从右边的点(.)拆分为两部分,兼容a.b.c的等情况
745
            $pos = strrpos($config['name'], '.');
4✔
746
            $service = substr($config['name'], 0, $pos);
4✔
747
            $option = substr($config['name'], $pos + 1);
4✔
748
            $data[$service][$option] = $this->decode($config['value'], $config['type']);
4✔
749
        }
750

751
        $this->setPhpFileCache('global-config', $data);
4✔
752

753
        return $data;
4✔
754
    }
755

756
    /**
757
     * @param string $key
758
     * @param mixed $value
759
     * @internal
760
     */
761
    protected function setPhpFileCache(string $key, $value)
762
    {
763
        $file = $this->phpFileCache->getDir() . '/' . $key . '.php';
4✔
764
        $this->writeConfig($file, $value);
4✔
765
    }
2✔
766

767
    /**
768
     * @param string $key
769
     * @param mixed $default
770
     * @return mixed
771
     * @internal
772
     */
773
    protected function getPhpFileCache(string $key, $default = null)
774
    {
775
        $file = $this->phpFileCache->getDir() . '/' . $key . '.php';
4✔
776
        if (is_file($file)) {
4✔
777
            return require $file;
4✔
778
        }
779
        return $default;
780
    }
781

782
    /**
783
     * @param array $values
784
     * @return array
785
     * @internal
786
     */
787
    protected function getNullKeys(array $values): array
788
    {
789
        $nulls = [];
44✔
790
        foreach ($values as $name => $value) {
44✔
791
            if (null === $value) {
44✔
792
                $nulls[] = $name;
28✔
793
            }
794
        }
795
        return $nulls;
44✔
796
    }
797
}
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