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

miaoxing / plugin / 5980801443

25 Aug 2023 10:05PM UTC coverage: 39.08% (-0.04%) from 39.123%
5980801443

push

github

semantic-release-bot
chore(release): publish

See CHANGELOG.md for more details.

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

95.59
/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];
42✔
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);
12✔
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);
66✔
124
        $nulls = $this->getNullKeys($values);
66✔
125

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

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

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

142
        return $values;
66✔
143
    }
144

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

153
    /**
154
     * @svc
155
     */
156
    protected function getSection(string $name): array
157
    {
158
        return array_merge(
30✔
159
            (array) $this->wei->getConfig($name),
30✔
160
            $this->getGlobalSection($name),
30✔
161
            $this->getAppSection($name)
30✔
162
        );
20✔
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];
96✔
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);
120✔
181
    }
182

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

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

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

207
    /**
208
     * @svc
209
     */
210
    protected function getGlobalSection(string $name): array
211
    {
212
        return $this->getSectionBy(GlobalConfigModel::class, $name);
36✔
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];
48✔
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);
60✔
231
    }
232

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

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

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

257
    /**
258
     * @svc
259
     */
260
    protected function getAppSection(string $name): array
261
    {
262
        return $this->getSectionBy(ConfigModel::class, $name);
36✔
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);
12✔
275
        $options = $this->getSection($name);
12✔
276
        return $this->wei->newInstance($name, $options);
12✔
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();
12✔
289
        $name = $this->wei->getServiceName($name);
12✔
290
        if (!isset($this->services[$appId][$name])) {
12✔
291
            $this->services[$appId][$name] = $this->createService($name);
6✔
292
        }
293
        return $this->services[$appId][$name];
12✔
294
    }
295

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

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

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

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

328
    /**
329
     * Update config model value to cache
330
     *
331
     * @param ConfigModel|GlobalConfigModel $model
332
     * @return $this
333
     * @experimental
334
     * @svc
335
     */
336
    protected function updateCache($model): self
337
    {
338
        $prefix = $this->getPrefix(get_class($model));
×
339
        $this->cache->set($prefix . $model->name, $this->decode($model->value, $model->type));
×
340
        return $this;
×
341
    }
342

343
    /**
344
     * Remove config model value cache
345
     *
346
     * @param ConfigModel|GlobalConfigModel $model
347
     * @return $this
348
     * @experimental
349
     * @svc
350
     */
351
    protected function deleteCache($model): self
352
    {
353
        $this->cache->delete($this->getPrefix(get_class($model)) . $model->name);
×
354
        return $this;
×
355
    }
356

357
    /**
358
     * @return string
359
     * @internal
360
     */
361
    protected function updatePreloadVersion(): string
362
    {
363
        $version = date('Y-m-d H:i:s');
12✔
364
        $this->setGlobal($this->getPreloadVersionKey(), $version, ['preload' => true]);
12✔
365
        return $version;
12✔
366
    }
367

368
    /**
369
     * @param string|class-string<ModelTrait> $model
370
     * @param array $names
371
     * @param array $defaults
372
     * @return array
373
     */
374
    protected function getMultipleBy(string $model, array $names, array $defaults): array
375
    {
376
        $this->missing = [];
222✔
377
        $prefix = $this->getPrefix($model);
222✔
378

379
        // From cache
380
        [$values, $missing] = $this->getMultipleFromCache($prefix, $names);
222✔
381

382
        // From database
383
        if ($missing) {
222✔
384
            [$dbValues, $missing] = $this->getMultipleFromDb($model, $names);
204✔
385
            $values = array_merge($values, $dbValues);
204✔
386

387
            // Next time will fetch from cache
388
            $cacheValues = $dbValues;
204✔
389
            foreach ($missing as $name) {
204✔
390
                $cacheValues[$name] = null;
108✔
391
            }
392
            $this->cacheWithPrefix($prefix, function () use ($cacheValues) {
136✔
393
                $this->cache->setMultiple($cacheValues);
204✔
394
            });
204✔
395
        }
396
        $this->missing = $missing;
222✔
397

398
        // Add default value
399
        foreach ($values as $name => $value) {
222✔
400
            $values[$name] = $value ?? $defaults[$name] ?? null;
222✔
401
        }
402

403
        return $values;
222✔
404
    }
405

406
    /**
407
     * @param string $prefix
408
     * @param array $names
409
     * @return array
410
     * @internal
411
     */
412
    protected function getMultipleFromCache(string $prefix, array $names): array
413
    {
414
        // From cache
415
        if (1 === count($names)) {
222✔
416
            $key = current($names);
180✔
417
            $values = [$key => $this->cache->get($prefix . $key)];
180✔
418
        } else {
419
            $values = $this->cacheWithPrefix($prefix, function () use ($names) {
32✔
420
                return $this->cache->getMultiple($names);
48✔
421
            });
48✔
422
        }
423

424
        $missing = [];
222✔
425
        foreach ($values as $name => $value) {
222✔
426
            if (!$this->cache->isHit($prefix . $name)) {
222✔
427
                $missing[] = $name;
204✔
428
            }
429
        }
430

431
        return [$values, $missing];
222✔
432
    }
433

434
    /**
435
     * @param string|class-string<ModelTrait> $model
436
     * @param array $names
437
     * @return array
438
     * @internal
439
     */
440
    protected function getMultipleFromDb(string $model, array $names): array
441
    {
442
        $configs = $model::select(['name', 'type', 'value'])
198✔
443
            ->where('name', $names)
198✔
444
            ->fetchAll();
198✔
445

446
        $values = [];
198✔
447
        foreach ($configs as $config) {
198✔
448
            $values[$config['name']] = $this->decode($config['value'], $config['type']);
135✔
449
        }
450

451
        $missing = [];
198✔
452
        foreach ($names as $name) {
198✔
453
            if (!array_key_exists($name, $values)) {
198✔
454
                $missing[] = $name;
102✔
455
            }
456
        }
457

458
        return [$values, $missing];
198✔
459
    }
460

461
    /**
462
     * @param string|class-string<ModelTrait> $model
463
     * @param array $values
464
     * @param array $options
465
     * @return $this
466
     */
467
    protected function setMultipleBy(string $model, array $values, array $options = []): self
468
    {
469
        if (!$values) {
216✔
470
            return $this;
6✔
471
        }
472

473
        $configs = $model::where('name', array_keys($values))
210✔
474
            ->indexBy('name')
210✔
475
            ->all();
210✔
476

477
        foreach ($values as $name => $value) {
210✔
478
            if (!isset($configs[$name])) {
210✔
479
                $configs[$name] = $model::fromArray(['name' => $name]);
96✔
480
            }
481

482
            [$value, $type] = $this->encode($value);
210✔
483
            $configs[$name]->fromArray([
210✔
484
                'type' => $type,
210✔
485
                'value' => $value,
210✔
486
            ]);
140✔
487

488
            // For global config
489
            if (isset($options['preload'])) {
210✔
490
                $configs[$name]->preload = $options['preload'];
18✔
491
            }
492
        }
493

494
        $configs->save();
210✔
495

496
        $data = [];
210✔
497
        $prefix = $this->getPrefix($model);
210✔
498
        foreach ($values as $key => $value) {
210✔
499
            $data[$prefix . $key] = $value;
210✔
500
        }
501
        $this->cache->setMultiple($data);
210✔
502

503
        return $this;
210✔
504
    }
505

506
    /**
507
     * @param string|class-string<ModelTrait> $model
508
     * @param string $name
509
     * @return $this
510
     * @internal
511
     */
512
    protected function deleteBy(string $model, string $name): self
513
    {
514
        $this->cache->delete($this->getPrefix($model) . $name);
66✔
515

516
        $config = $model::findBy('name', $name);
66✔
517
        $config && $config->destroy();
66✔
518

519
        return $this;
66✔
520
    }
521

522
    /**
523
     * @param string|class-string<ModelTrait> $model
524
     * @param string $name
525
     * @return array
526
     */
527
    protected function getSectionBy(string $model, string $name): array
528
    {
529
        // From database
530
        $configs = $model::select(['name', 'type', 'value'])
42✔
531
            ->where('name', 'like', $name . '.%')
42✔
532
            ->fetchAll();
42✔
533

534
        $length = strlen($name) + 1;
42✔
535
        $values = [];
42✔
536
        foreach ($configs as $config) {
42✔
537
            $values[substr($config['name'], $length)] = $this->decode($config['value'], $config['type']);
42✔
538
        }
539

540
        return $values;
42✔
541
    }
542

543
    /**
544
     * @param string $model
545
     * @return string
546
     */
547
    protected function getPrefix(string $model): string
548
    {
549
        if (ConfigModel::class === $model) {
270✔
550
            return 'config:' . $this->app->getId() . ':';
156✔
551
        }
552
        return 'globalConfig:';
180✔
553
    }
554

555
    /**
556
     * @param string $prefix
557
     * @param callable $fn
558
     * @return mixed
559
     * @internal
560
     */
561
    protected function cacheWithPrefix(string $prefix, callable $fn)
562
    {
563
        $namespace = $this->cache->getNamespace();
204✔
564
        $this->cache->setNamespace($namespace . $prefix);
204✔
565
        $result = $fn();
204✔
566
        $this->cache->setNamespace($namespace);
204✔
567
        return $result;
204✔
568
    }
569

570
    /**
571
     * Convert PHP value to config value
572
     *
573
     * @param mixed $value
574
     * @return array
575
     * @internal
576
     */
577
    protected function encode($value): array
578
    {
579
        $type = gettype($value);
210✔
580
        if (isset($this->scalarTypes[$type])) {
210✔
581
            return [$value, $this->scalarTypes[$type]];
186✔
582
        }
583

584
        if ('array' === $type) {
24✔
585
            return [json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE), static::TYPE_ARRAY];
12✔
586
        }
587

588
        if ($value instanceof \stdClass) {
12✔
589
            return [json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE), static::TYPE_JSON];
6✔
590
        }
591

592
        return [serialize($value), static::TYPE_OBJECT];
6✔
593
    }
594

595
    /**
596
     * Convert config value to PHP value
597
     *
598
     * @param mixed $value
599
     * @param string $type
600
     * @return mixed
601
     * @internal
602
     */
603
    protected function decode($value, string $type)
604
    {
605
        switch ($type) {
606
            case static::TYPE_STRING:
183✔
607
                return (string) $value;
99✔
608

609
            case static::TYPE_INT:
99✔
610
                return (int) $value;
51✔
611

612
            case static::TYPE_FLOAT:
48✔
613
                return (float) $value;
6✔
614

615
            case static::TYPE_BOOL:
42✔
616
                return filter_var($value, \FILTER_VALIDATE_BOOLEAN);
12✔
617

618
            case static::TYPE_ARRAY:
30✔
619
                return (array) json_decode($value, true);
12✔
620

621
            case static::TYPE_JSON:
18✔
622
                return json_decode($value);
6✔
623

624
            case static::TYPE_OBJECT:
12✔
625
                return unserialize($value);
6✔
626

627
            case static::TYPE_NULL:
6✔
628
                return null;
6✔
629

630
            default:
631
                return $value;
×
632
        }
633
    }
634

635
    /**
636
     * 获取配置文件
637
     *
638
     * @return string
639
     */
640
    protected function getLocalFile(): string
641
    {
642
        return str_replace('%env%', $this->env->getName(), $this->localFile);
12✔
643
    }
644

645
    /**
646
     * 更新配置到本地文件中
647
     *
648
     * @param array $configs
649
     * @svc
650
     */
651
    protected function updateLocal(array $configs)
652
    {
653
        $file = $this->getLocalFile();
6✔
654
        if (is_file($file)) {
6✔
655
            $localConfigs = require $file;
6✔
656
        } else {
657
            $localConfigs = [];
6✔
658
        }
659

660
        foreach ($configs as $name => $value) {
6✔
661
            if (isset($localConfigs[$name])) {
6✔
662
                $localConfigs[$name] = array_merge($localConfigs[$name], $value);
6✔
663
            } else {
664
                $localConfigs[$name] = $value;
6✔
665
            }
666
        }
667

668
        $this->writeConfig($file, $localConfigs);
6✔
669
        $this->wei->setConfig($configs);
6✔
670
    }
2✔
671

672
    /**
673
     * @param string $file
674
     * @param mixed $configs
675
     */
676
    protected function writeConfig(string $file, $configs): void
677
    {
678
        $content = $this->generateContent($configs);
12✔
679
        file_put_contents($file, $content);
12✔
680
        function_exists('opcache_invalidate') && opcache_invalidate($file);
12✔
681
    }
4✔
682

683
    /**
684
     * 将数据库读出的对象生成文件内容
685
     *
686
     * @param array $configs
687
     * @return string
688
     */
689
    protected function generateContent($configs)
690
    {
691
        return "<?php\n\nreturn " . $this->varExport($configs) . ";\n";
12✔
692
    }
693

694
    /**
695
     * @param mixed $var
696
     * @param string $indent
697
     * @return string
698
     * @link https://stackoverflow.com/questions/24316347/how-to-format-var-export-to-php5-4-array-syntax
699
     */
700
    protected function varExport($var, $indent = '')
701
    {
702
        switch (gettype($var)) {
12✔
703
            case 'string':
12✔
704
                return '\'' . addcslashes($var, "\\\$\\'\r\n\t\v\f") . '\'';
12✔
705
            case 'array':
12✔
706
                $indexed = array_keys($var) === range(0, count($var) - 1);
12✔
707
                $result = [];
12✔
708
                foreach ($var as $key => $value) {
12✔
709
                    $result[] = $indent . '    '
12✔
710
                        . ($indexed ? '' : $this->varExport($key) . ' => ')
12✔
711
                        . $this->varExport($value, "$indent    ");
12✔
712
                }
713

714
                return "[\n" . implode(",\n", $result) . ($result ? ',' : '') . "\n" . $indent . ']';
12✔
715
            case 'boolean':
12✔
716
                return $var ? 'true' : 'false';
6✔
717

718
            case 'NULL':
12✔
719
                return 'null';
6✔
720

721
            case 'object':
12✔
722
                if (isset($var->express)) {
×
723
                    return $var->express;
×
724
                }
725
            // no break
726

727
            default:
728
                return var_export($var, true);
12✔
729
        }
730
    }
731

732
    /**
733
     * 判断本地的配置是否需要更改
734
     *
735
     * @param array $configs
736
     * @return bool
737
     * @internal
738
     */
739
    protected function needsUpdatePreload(array $configs): bool
740
    {
741
        $key = $this->getPreloadVersionKey();
6✔
742

743
        $version = $this->getGlobal($key);
6✔
744
        if (!$version) {
6✔
745
            $version = $this->updatePreloadVersion();
6✔
746
        }
747

748
        [$service, $option] = explode('.', $key);
6✔
749
        return !isset($configs[$service][$option]) || $configs[$service][$option] < $version;
6✔
750
    }
751

752
    /**
753
     * @return string
754
     * @svc
755
     */
756
    protected function getPreloadVersionKey(): string
757
    {
758
        return 'config.preloadVersion';
12✔
759
    }
760

761
    /**
762
     * @return array
763
     * @internal
764
     */
765
    protected function setPreloadCache(): array
766
    {
767
        $configs = GlobalConfigModel::select('name', 'type', 'value')
6✔
768
            ->where('preload', true)
6✔
769
            ->fetchAll();
6✔
770

771
        $data = [];
6✔
772
        foreach ($configs as $config) {
6✔
773
            // 从右边的点(.)拆分为两部分,兼容a.b.c的等情况
774
            $pos = strrpos($config['name'], '.');
6✔
775
            $service = substr($config['name'], 0, $pos);
6✔
776
            $option = substr($config['name'], $pos + 1);
6✔
777
            $data[$service][$option] = $this->decode($config['value'], $config['type']);
6✔
778
        }
779

780
        $this->setPhpFileCache('global-config', $data);
6✔
781

782
        return $data;
6✔
783
    }
784

785
    /**
786
     * @param string $key
787
     * @param mixed $value
788
     * @internal
789
     */
790
    protected function setPhpFileCache(string $key, $value)
791
    {
792
        $file = $this->phpFileCache->getDir() . '/' . $key . '.php';
6✔
793
        $this->writeConfig($file, $value);
6✔
794
    }
2✔
795

796
    /**
797
     * @param string $key
798
     * @param mixed $default
799
     * @return mixed
800
     * @internal
801
     */
802
    protected function getPhpFileCache(string $key, $default = null)
803
    {
804
        $file = $this->phpFileCache->getDir() . '/' . $key . '.php';
6✔
805
        if (is_file($file)) {
6✔
806
            return require $file;
6✔
807
        }
808
        return $default;
×
809
    }
810

811
    /**
812
     * @param array $values
813
     * @return array
814
     * @internal
815
     */
816
    protected function getNullKeys(array $values): array
817
    {
818
        $nulls = [];
66✔
819
        foreach ($values as $name => $value) {
66✔
820
            if (null === $value) {
66✔
821
                $nulls[] = $name;
42✔
822
            }
823
        }
824
        return $nulls;
66✔
825
    }
826
}
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