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

liqueurdetoile / wordpress-bundler / 3842803254

pending completion
3842803254

push

github

Liqueur de Toile
tests: Fix tests

328 of 459 relevant lines covered (71.46%)

8.19 hits per line

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

61.44
/src/Config.php
1
<?php
2
declare(strict_types=1);
3

4
namespace Lqdt\WordpressBundler;
5

6
use Adbar\Dot;
7
use InvalidArgumentException;
8
use JsonException;
9
use Lqdt\WordpressBundler\Exception\MissingKeyConfigException;
10
use RuntimeException;
11

12
/**
13
 * Utility class to manage configurations.
14
 *
15
 * It handles three layers of logic for maximum flexibility:
16
 *
17
 * - Fallbacks layer
18
 * - Defaults layer
19
 * - Overrides layer
20
 *
21
 * When looking for a given key in a configuration setup, Config will first try to find it in overrides, then in defaults and finally in fallbacks
22
 * When exporting a full config, keys will be merged from defaults to overrides to retain highest priority layer value
23
 *
24
 * Config also embeds import and export of configurations from JSON files
25
 *
26
 * @author Liqueur de Toile <contact@liqueurdetoile.com>
27
 * @copyright 2022-present Liqueur de Toile
28
 * @license GPL-3.0-or-later (https://www.gnu.org/licenses/gpl-3.0.html)
29
 */
30
class Config
31
{
32
    /**
33
     * Stores default configuration and enabled dot notation access
34
     *
35
     * @var \Adbar\Dot
36
     * @psalm-suppress PropertyNotSetInConstructor
37
     */
38
    protected $_defaults;
39

40
    /**
41
     * Stores fallback configuration that may be used if required key is missing in overrides and default configuration
42
     *
43
     * @var \Adbar\Dot
44
     * @psalm-suppress PropertyNotSetInConstructor
45
     */
46
    protected $_fallbacks;
47

48
    /**
49
     * Stores configuration overrides that have priority on default and fallback settings
50
     *
51
     * @var \Adbar\Dot
52
     * @psalm-suppress PropertyNotSetInConstructor
53
     */
54
    protected $_overrides;
55

56
    /**
57
     * Stores path to last loaded configuration file
58
     *
59
     * @var string|null
60
     */
61
    protected $_path;
62

63
    /**
64
     * Stores key used with last loaded configuration file. Key can be a dotted path
65
     *
66
     * @var string|null
67
     */
68
    protected $_key;
69

70
    /**
71
     * Creates a configurator instance for easier manipulation as it exposes convenient getter and setters
72
     * Different configurations can be provided as arguments
73
     *
74
     * @param array|\Adbar\Dot $defaults  Default full configuration
75
     * @param array|\Adbar\Dot $fallbacks Fallback configuration
76
     * @param array|\Adbar\Dot $overrides Configuration overrides
77
     */
78
    final public function __construct($defaults = [], $fallbacks = [], $overrides = [])
79
    {
80
        $this
28✔
81
            ->setDefaults($defaults)
28✔
82
            ->setFallbacks($fallbacks)
28✔
83
            ->setOverrides($overrides);
28✔
84
    }
85

86
    /**
87
     * Returns a configuration instance
88
     *
89
     * @param array|\Adbar\Dot $defaults  Default full configuration
90
     * @param array|\Adbar\Dot $fallbacks Fallback configuration
91
     * @param array|\Adbar\Dot $overrides Configuration overrides
92
     * @return \Lqdt\WordpressBundler\Config
93
     */
94
    public static function getInstance($defaults = [], $fallbacks = [], $overrides = []): Config
95
    {
96
        return new static($defaults, $fallbacks, $overrides);
9✔
97
    }
98

99
    /**
100
     * Validates and normalizes a path to a file. If no path is provided, validation
101
     * will returns the path to the root composer.json in project
102
     *
103
     * @param string|null $path  Path to validate
104
     * @param bool     $throw If true, a missing file will throw a RuntimeException
105
     * @throws \RuntimeException If no file can be found at normalized target path
106
     * @return string
107
     */
108
    public static function getConfigFilePath(?string $path = null, bool $throw = true): string
109
    {
110
        $basepath = Resolver::makeAbsolute($path);
35✔
111

112
        if (is_dir($basepath)) {
35✔
113
            $basepath = Resolver::makeAbsolute('/composer.json', $basepath);
×
114
        }
115

116
        if (!is_file($basepath) && $throw) {
35✔
117
            throw new RuntimeException(sprintf('[WordpressBundler] No configuration file found at %s', $basepath));
6✔
118
        }
119

120
        return $basepath;
35✔
121
    }
122

123
    /**
124
     * Reads content of a JSON file
125
     *
126
     * If no path is provided, composer.json at the root project will be used
127
     * An optional key into configuration can be provided to target a part of the configuration file
128
     *
129
     * @param  string|null $path  Path to JSON configuration file. Defaults to composer.json at root project
130
     * @param  string|null $key   Optional key in configuration to read from
131
     * @param  bool     $throw If `true`, any error in accessing file content will raise an exception. Otherwise an empty Dot object is returned
132
     * @return \Adbar\Dot
133
     * @throws \RuntimeException Whenever reading of the file is not succesful
134
     */
135
    public static function read(?string $path = null, ?string $key = null, bool $throw = true): Dot
136
    {
137
        $path = self::getConfigFilePath($path ?? 'composer.json');
35✔
138
        $content = file_get_contents($path);
35✔
139

140
        if ($content === false) {
35✔
141
            if (!$throw) {
×
142
                return new Dot();
×
143
            }
144

145
            throw new RuntimeException(
×
146
                sprintf('[WordpressBundler] Unable to read configuration file "%s"', $path)
×
147
            );
×
148
        }
149

150
        try {
151
            $content = new Dot(json_decode($content, true, 512, JSON_THROW_ON_ERROR));
35✔
152
        } catch (JsonException $err) {
×
153
            if (!$throw) {
×
154
                return new Dot();
×
155
            }
156

157
            throw new RuntimeException(
×
158
                sprintf('[WordpressBundler] Unable to parse content as JSON in "%s"', $path)
×
159
            );
×
160
        }
161

162
        if ($key !== null) {
35✔
163
            if (!$content->has($key)) {
23✔
164
                if (!$throw) {
6✔
165
                    return new Dot();
1✔
166
                }
167

168
                throw new RuntimeException(
5✔
169
                    sprintf('[WordpressBundler] Unable to locate requested key "%s" in "%s"', $key, $path)
5✔
170
                );
5✔
171
            }
172

173
            $content = new Dot($content->get($key));
19✔
174
        }
175

176
        return $content;
31✔
177
    }
178

179
    /**
180
     * Saves a configuration as JSON. Creates a new file or overwrites content unless merge parameter is set to true
181
     *
182
     * If no path is provided, composer.json at the root project will be used
183
     * An optional key into configuration can be provided to target a part of the configuration file
184
     *
185
     * @param array|\Adbar\Dot $config Configuration to save
186
     * @param string           $path   Target file.  Defaults to composer.json at root project
187
     * @param string           $key    Key in target file
188
     * @param bool          $merge  If true, the configuration will be merged into configuration
189
     * @return string Path to target file
190
     */
191
    public static function write($config, ?string $path = null, ?string $key = null, bool $merge = false): string
192
    {
193
        $path = self::getConfigFilePath($path);
10✔
194
        $source = self::read($path, $key, false);
10✔
195
        $config = new Dot($config);
10✔
196
        $source = $merge ?
10✔
197
            $source->mergeRecursiveDistinct($config) :
3✔
198
            $config;
10✔
199
        $data = $key === null ? $source : (new Dot())->set($key, $source);
10✔
200

201

202
        file_put_contents($path, json_encode($data->all(), JSON_PRETTY_PRINT), LOCK_EX);
10✔
203

204
        return $path;
10✔
205
    }
206

207
    /**
208
     * Returns the fallbacks registry
209
     *
210
     * @return \Adbar\Dot
211
     */
212
    public function &getFallbacks(): Dot
213
    {
214
        return $this->_fallbacks;
×
215
    }
216

217
    /**
218
     * Sets configuration fallbacks registry
219
     *
220
     * @param array|\Adbar\Dot $fallbacks Fallback values
221
     * @return self
222
     */
223
    public function setFallbacks($fallbacks)
224
    {
225
        $this->_fallbacks = new Dot($fallbacks);
28✔
226

227
        return $this;
28✔
228
    }
229

230

231
    /**
232
     * Returns the defaults registry
233
     *
234
     * @return \Adbar\Dot
235
     */
236
    public function &getDefaults(): Dot
237
    {
238
        return $this->_defaults;
20✔
239
    }
240

241
    /**
242
     * Sets configuration defaults registry
243
     *
244
     * @param array|\Adbar\Dot $defaults Default values
245
     * @return self
246
     */
247
    public function setDefaults($defaults): self
248
    {
249
        $this->_defaults = new Dot($defaults);
28✔
250

251
        return $this;
28✔
252
    }
253

254
    /**
255
     * Gets the overrides registry
256
     *
257
     * @return \Adbar\Dot
258
     */
259
    public function &getOverrides(): Dot
260
    {
261
        return $this->_overrides;
6✔
262
    }
263

264
    /**
265
     * Sets configuration overrides
266
     *
267
     * @param array|\Adbar\Dot $overrides Overrides values
268
     * @return self
269
     */
270
    public function setOverrides($overrides): self
271
    {
272
        $this->_overrides = new Dot($overrides);
28✔
273

274
        return $this;
28✔
275
    }
276

277
    /**
278
     * Returns the current whole config as a registry by merging the fallbacks, default and overrides registry based on highest priority
279
     *
280
     * @return \Adbar\Dot
281
     */
282
    public function getConfig(): Dot
283
    {
284
        $config = new Dot($this->_fallbacks);
25✔
285

286
        return $config
25✔
287
            ->mergeRecursiveDistinct($this->_defaults)
25✔
288
            ->mergeRecursiveDistinct($this->_overrides);
25✔
289
    }
290

291
    /**
292
     * Load configuration from a json file into this instance
293
     *
294
     * If no path is provided, composer.json at the root project will be used
295
     * An optional key into configuration can be provided to target a path into the configuration file. Dotted notation to target nested keys is allowed.
296
     *
297
     * A specific registry may be targetted of needed.
298
     *
299
     * @param  string|null $path     Path to JSON configuration file. Defaults to composer.json at root project
300
     * @param  string|null $key      Optional key in configuration to read from
301
     * @param  string      $registry Configuration registry to load configuration into. Can be any between fallbacks, overrides and defaults
302
     * @return self
303
     */
304
    public function load(?string $path = null, ?string $key = null, string $registry = 'defaults'): self
305
    {
306
        $path = self::getConfigFilePath($path);
25✔
307
        $method = 'set' . ucFirst($registry);
25✔
308
        if (!method_exists($this, $method)) {
25✔
309
            throw new InvalidArgumentException(
×
310
                sprintf(
×
311
                    '[WordpressBundler] Target config registry is not valid. ' .
×
312
                    'Expecting defaults, fallbacks or overrides and get %s',
×
313
                    $registry
×
314
                )
×
315
            );
×
316
        }
317
        $this->{$method}(self::read($path, $key));
25✔
318
        $this->_path = $path;
21✔
319
        $this->_key = $key;
21✔
320

321
        return $this;
21✔
322
    }
323

324
    /**
325
     * Saves a configuration registry in a file as JSON
326
     *
327
     * If no path or `null` is provided, latest loaded configuration file will be used
328
     * If no key or `null` is provided, latest used key will be used
329
     *
330
     * @param  string      $registry Configuration registry to save. Can be any between fallbacks, overrides, defaults and config
331
     * @param  string|null $path     Path to JSON configuration file. Defaults to composer.json at root project
332
     * @param  string|null $key      Optional key in configuration to read from
333
     * @param bool     $merge    If true, the configuration will be merged into configuration already present in file
334
     * @return self
335
     */
336
    public function save(
337
        string $registry = 'defaults',
338
        ?string $path = null,
339
        ?string $key = null,
340
        bool $merge = false
341
    ): self {
342
        $method = 'get' . ucFirst($registry);
3✔
343
        if (!method_exists($this, $method)) {
3✔
344
            throw new InvalidArgumentException(
×
345
                sprintf(
×
346
                    '[WordpressBundler] Source config registry is not valid. ' .
×
347
                    'Expecting defaults, fallbacks, overrides or config and get %s',
×
348
                    $registry
×
349
                )
×
350
            );
×
351
        }
352

353
        /** @var \Adbar\Dot $data */
354
        $data = $this->{$method}();
3✔
355
        self::write($data, $path ?? $this->_path, $key ?? $this->_key, $merge);
3✔
356

357
        return $this;
3✔
358
    }
359

360
    /**
361
     * Ensurs that a given key exists in configuration and is not null
362
     *
363
     * @param string $key Key to check
364
     * @return bool
365
     */
366
    public function has(string $key): bool
367
    {
368
        $config = $this->getConfig();
×
369

370
        return $config->has($key) && $config->get($key) !== null;
×
371
    }
372

373
    /**
374
     * Returns the whole configuration with merged priorities
375
     *
376
     * @return array
377
     */
378
    public function all(): array
379
    {
380
        return $this->getConfig()->all();
3✔
381
    }
382

383
    /**
384
     * Get whole a configuration or a targetted key. Dotted keys are allowed to access nested values.
385
     *
386
     * If the key is missing, the fallback value will be returned
387
     *
388
     * @param  string $key Key to fetch
389
     * @param  mixed $fallback
390
     * @return mixed
391
     * @throws \RuntimeException If missing key and no fallback defined
392
     */
393
    public function get(string $key, $fallback = null)
394
    {
395
        $config = $this->getConfig();
9✔
396

397
        return $config->has($key) ? $config->get($key) : $fallback;
9✔
398
    }
399

400
    /**
401
     * Get whole a configuration or a targetted key. Dotted keys are allowed to access nested values.
402
     *
403
     * If the key is missing or null, an exception will be raised
404
     *
405
     * @param  string $key Key to fetch
406
     * @return mixed
407
     * @throws \Lqdt\WordpressBundler\Exception\MissingKeyConfigException If missing key and no fallback defined
408
     */
409
    public function getOrFail(string $key)
410
    {
411
        $config = $this->getConfig();
20✔
412

413
        if ($config->has($key)) {
20✔
414
            return $config->get($key);
18✔
415
        }
416

417
        throw new MissingKeyConfigException($key);
2✔
418
    }
419

420
    /**
421
     * Fetches a value in config and ensures that correct type is returned through a validator
422
     *
423
     * @param string   $key       Key to fetch
424
     * @param string   $type      Expected data type as string
425
     * @param callable $validator Validator callback
426
     * @param mixed    $fallback
427
     * @return mixed
428
     * @throws \TypeError   If returned value is not validated
429
     */
430
    public function getWithType(string $key, string $type, callable $validator, $fallback = null)
431
    {
432
        /** @psalm-suppress MixedAssignment */
433
        $v = $fallback === null ? $this->getOrFail($key) : $this->get($key, $fallback);
18✔
434

435
        if (!call_user_func($validator, $v)) {
18✔
436
            throw new \TypeError(
16✔
437
                sprintf('Wrong value type for key %s. Expecting %s and got %s', $key, $type, gettype($v))
16✔
438
            );
16✔
439
        }
440

441
        return $v;
18✔
442
    }
443

444
    /**
445
     * Fetch a value by key and check that it is an array
446
     *
447
     * @param  string $key Key
448
     * @param  array|null $fallback Fallback value
449
     * @return array
450
     */
451
    public function getArray(string $key, ?array $fallback = null): array
452
    {
453
        /** @var array */
454
        return $this->getWithType($key, 'array', 'is_array', $fallback);
14✔
455
    }
456

457
    /**
458
     * Fetch a value by key and check that it is a bool
459
     *
460
     * @param  string $key Key
461
     * @param  bool|null $fallback Fallback value
462
     * @return bool
463
     */
464
    public function getBoolean(string $key, ?bool $fallback = null): bool
465
    {
466
        /** @var bool */
467
        return $this->getWithType($key, 'bool', 'is_bool', $fallback);
17✔
468
    }
469

470
    /**
471
     * Fetch a value by key and check that it is an int
472
     *
473
     * @param  string $key Key
474
     * @param  int|null $fallback Fallback value
475
     * @return int
476
     * @psalm-suppress MixedReturnStatement
477
     * @psalm-suppress MixedInferredReturnType
478
     */
479
    public function getInt(string $key, ?int $fallback = null): int
480
    {
481
        /** @var int */
482
        return $this->getWithType($key, 'int', 'is_int', $fallback);
16✔
483
    }
484

485
    /**
486
     * Fetch a value by key and check that it is a string
487
     *
488
     * @param  string $key Key
489
     * @param  string|null $fallback Fallback value
490
     * @return string
491
     * @psalm-suppress MixedReturnStatement
492
     * @psalm-suppress MixedInferredReturnType
493
     */
494
    public function getString(string $key, ?string $fallback = null): string
495
    {
496
        /** @var string */
497
        return $this->getWithType($key, 'string', 'is_string', $fallback);
17✔
498
    }
499

500
    /**
501
     * Sets a value by key in default configuration.  Dotted keys are allowed to set up nested values. If the key does not exist, it will be created
502
     * To persist updates to configuration file, a subsequent call to `Config::save` must be done.
503
     *
504
     * @param  string $key      Key to set
505
     * @param  mixed  $value    Value
506
     * @param string $registry Targetted registry.  Can be any between fallbacks, overrides, defaults
507
     * @return self
508
     */
509
    public function set(string $key, $value, string $registry = 'defaults'): self
510
    {
511
        switch ($registry) {
512
            case 'fallbacks':
8✔
513
                $this->_fallbacks->set($key, $value);
×
514
                break;
×
515
            case 'defaults':
8✔
516
                $this->_defaults->set($key, $value);
8✔
517
                break;
8✔
518
            case 'overrides':
2✔
519
                $this->_overrides->set($key, $value);
2✔
520
                break;
2✔
521
            default:
522
                throw new InvalidArgumentException(
×
523
                    sprintf(
×
524
                        '[WordpressBundler] Target config registry for dropping key is not valid. ' .
×
525
                        'Expecting defaults, fallback or overrides and get %s',
×
526
                        $registry
×
527
                    )
×
528
                );
×
529
        }
530

531
        return $this;
8✔
532
    }
533

534
    /**
535
     * Merges a configuration into a given registry
536
     *
537
     * @param array $config
538
     * @param string $registry
539
     * @throws \InvalidArgumentException
540
     * @return \Lqdt\WordpressBundler\Config
541
     */
542
    public function merge(array $config, string $registry = 'defaults')
543
    {
544
        $method = 'get' . ucFirst($registry);
17✔
545
        if (!method_exists($this, $method)) {
17✔
546
            throw new InvalidArgumentException(
×
547
                sprintf(
×
548
                    '[WordpressBundler] Source config registry is not valid. ' .
×
549
                    'Expecting defaults, fallbacks or overrides and get %s',
×
550
                    $registry
×
551
                )
×
552
            );
×
553
        }
554

555
        /** @var \Adbar\Dot $registry */
556
        $registry = $this->{$method}();
17✔
557
        $registry->mergeRecursiveDistinct(new Dot($config));
17✔
558

559
        return $this;
17✔
560
    }
561

562
    /**
563
     * Removes one or many keys in one or all registries
564
     *
565
     * @param string|int|array $key      Key to remove
566
     * @param string               $registry Targetted registry or all registries with `config` value.  Can be any between fallbacks, overrides, defaults and config
567
     * @return self
568
     */
569
    public function delete($key, string $registry = 'defaults'): self
570
    {
571
        switch ($registry) {
572
            case 'fallbacks':
1✔
573
                $this->_fallbacks->delete($key);
×
574
                break;
×
575
            case 'defaults':
1✔
576
                $this->_defaults->delete($key);
1✔
577
                break;
1✔
578
            case 'overrides':
1✔
579
                $this->_overrides->delete($key);
1✔
580
                break;
1✔
581
            case 'config':
×
582
                $this->_fallbacks->delete($key);
×
583
                $this->_defaults->delete($key);
×
584
                $this->_overrides->delete($key);
×
585
                break;
×
586
            default:
587
                throw new InvalidArgumentException(
×
588
                    sprintf(
×
589
                        '[WordpressBundler] Target config registry for dropping key is not valid. ' .
×
590
                        'Expecting defaults, fallbacks, overrides or config and get %s',
×
591
                        $registry
×
592
                    )
×
593
                );
×
594
        }
595

596
        return $this;
1✔
597
    }
598
}
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

© 2026 Coveralls, Inc