AbstractConfig::setPathSeparator()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
rs 10
1
<?php
2
declare(strict_types=1);
3
4
namespace SKien\Config;
5
6
/**
7
 * Abstract base class for config components.
8
 *
9
 * @package Config
10
 * @author Stefanius <[email protected]>
11
 * @copyright MIT License - see the LICENSE file for details
12
 */
13
abstract class AbstractConfig implements ConfigInterface
14
{
15
    /** @var array<mixed> holding the config data    */
16
    protected ?array $aConfig = null;
17
    /** @var string format for date parameters (default: 'Y-m-d')    */
18
    protected string $strDateFormat = 'Y-m-d';
19
    /** @var string format for datetime parameters (default: 'Y-m-d H:i')    */
20
    protected string $strDateTimeFormat = 'Y-m-d H:i';
21
    /** @var string the path separator (default: '.')    */
22
    protected string $strSeparator = '.';
23
24
    /**
25
     * Set the format for date parameters.
26
     * See the formatting options DateTime::createFromFormat.
27
     * In most cases, the same letters as for the date() can be used.
28
     * @param string $strFormat
29
     * @link https://www.php.net/manual/en/datetime.createfromformat.php
30
     */
31
    public function setDateFormat(string $strFormat) : void
32
    {
33
        $this->strDateFormat = $strFormat;
34
    }
35
36
    /**
37
     * Set the format for datetime parameters.
38
     * See the formatting options DateTime::createFromFormat.
39
     * In most cases, the same letters as for the date() can be used.
40
     * @param string $strFormat
41
     * @link https://www.php.net/manual/en/datetime.createfromformat.php
42
     */
43
    public function setDateTimeFormat(string $strFormat) : void
44
    {
45
        $this->strDateTimeFormat = $strFormat;
46
    }
47
48
    /**
49
     * Set the separator character.
50
     * Default the '.' is used as separator.
51
     * @param string $strSeparator
52
     */
53
    public function setPathSeparator(string $strSeparator) : void
54
    {
55
        $this->strSeparator = $strSeparator;
56
    }
57
58
    /**
59
     * Get the value specified by path.
60
     * The path addresses a value within the configuration, which can be nested at
61
     * any depth (depending on the file format).
62
     * The individual levels are specified separated by a separator (default is '.').
63
     * The separator can be changed with `setPathSeparator ()`.
64
     * @see setPathSeparator()
65
     * @param string $strPath   the path to the value.
66
     * @param mixed $default    a default value that is returned if entry doesn't exist
67
     * @return mixed
68
     */
69
    public function getValue(string $strPath, $default = null)
70
    {
71
        if ($this->aConfig === null) {
72
            // without valid config file just return the default value
73
            return $default;
74
        }
75
        $aPath = $this->splitPath($strPath);
76
        $iDepth = count($aPath);
77
        $value = null;
78
        $aValues = $this->aConfig;
79
        for ($i = 0; $i < $iDepth; $i++) {
80
            if (!is_array($aValues)) {
81
                $value = null;
82
                break;
83
            }
84
            if (array_key_exists($aPath[$i], $aValues)) {
85
                $value = $aValues[$aPath[$i]];
86
                if ($value === null) {
87
                    // value exist but is set to null - force to empty string otherwise it would be changed to the default value!
88
                    $value = '';
89
                }
90
            } else {
91
                $value = null;
92
                break;
93
            }
94
            $aValues = $value;
95
        }
96
        return $value ?? $default;
97
    }
98
99
    /**
100
     * Get the string value specified by path.
101
     * @param string $strPath
102
     * @param string $strDefault
103
     * @return string
104
     */
105
    public function getString(string $strPath, string $strDefault = '') : string
106
    {
107
        return (string)$this->getValue($strPath, $strDefault);
108
    }
109
110
    /**
111
     * Get the integer value specified by path.
112
     * @param string $strPath
113
     * @param int $iDefault
114
     * @return int
115
     */
116
    public function getInt(string $strPath, int $iDefault = 0) : int
117
    {
118
        return intval($this->getValue($strPath, $iDefault));
119
    }
120
121
    /**
122
     * Get the float value specified by path.
123
     * @param string $strPath
124
     * @param float $fltDefault
125
     * @return float
126
     */
127
    public function getFloat(string $strPath, float $fltDefault = 0.0) : float
128
    {
129
        return floatval($this->getValue($strPath, $fltDefault));
130
    }
131
132
    /**
133
     * Get the boolean value specified by path.
134
     * @param string $strPath
135
     * @param bool $bDefault
136
     * @return bool
137
     */
138
    public function getBool(string $strPath, bool $bDefault = false) : bool
139
    {
140
        $value = $this->getValue($strPath, $bDefault);
141
        if (!is_bool($value)) {
142
            $value = $this->boolFromString((string)$value, $bDefault);
143
        }
144
        return $value;
145
    }
146
147
    /**
148
     * Get the date value specified by path.
149
     * If the config file contains an integer, it is seen as unix timestamp.
150
     * If the config file contains the date as string, it is parsed with the internal
151
     * date format set by `setDateFormat()` (default: 'Y-m-d')
152
     * @param string $strPath
153
     * @param int $default default value (unix timestamp)
154
     * @return int date as unix timestamp
155
     */
156
    public function getDate(string $strPath, int $default = 0) : int
157
    {
158
        $date = (string)$this->getValue($strPath, $default);
159
        if (!ctype_digit($date)) {
160
            $dt = \DateTime::createFromFormat($this->strDateFormat, $date);
161
            $date = $default;
162
            if ($dt !== false) {
163
                $aError = $dt->getLastErrors();
164
                if ($aError['error_count'] == 0) {
165
                    $dt->setTime(0, 0);
166
                    $date = $dt->getTimestamp();
167
                }
168
            }
169
        } else {
170
            $date = intval($date);
171
        }
172
        return $date;
173
    }
174
175
    /**
176
     * Get the date and time value specified by path as unix timestamp.
177
     * If the config file contains an integer, it is seen as unix timestamp.
178
     * If the config file contains the date-time as string, it is parsed with the internal
179
     * date format set by `setDateTimeFormat()` (default: 'Y-m-d H:i')
180
     * @param string $strPath
181
     * @param int $default default value (unix timestamp)
182
     * @return int unix timestamp
183
     */
184
    public function getDateTime(string $strPath, int $default = 0) : int
185
    {
186
        $date = (string)$this->getValue($strPath, $default);
187
        if (!ctype_digit($date)) {
188
            $dt = \DateTime::createFromFormat($this->strDateTimeFormat, $date);
189
            $date = $default;
190
            if ($dt !== false) {
191
                $aError = $dt->getLastErrors();
192
                if ($aError['error_count'] == 0) {
193
                    $date = $dt->getTimestamp();
194
                }
195
            }
196
        } else {
197
            $date = intval($date);
198
        }
199
        return $date;
200
    }
201
202
    /**
203
     * Get the array specified by path.
204
     * @param string $strPath
205
     * @param array<mixed> $aDefault
206
     * @return array<mixed>
207
     */
208
    public function getArray(string $strPath, array $aDefault = []) : array
209
    {
210
        $value = $this->getValue($strPath, $aDefault);
211
212
        // if $value is not an array, the entry exists (otherwise $value = $aDefault, which is an array!),
213
        // but only contains a single value! In this case we are return an array containing that single element!
214
        return is_array($value) ? $value : [$value];
215
    }
216
217
    /**
218
     * Returns the internal array.
219
     * @return array<mixed>
220
     */
221
    public function getConfig() : array
222
    {
223
        return $this->aConfig ?? [];
224
    }
225
226
    /**
227
     * Split the given path in its components.
228
     * @param string $strPath
229
     * @return array<string>
230
     */
231
    protected function splitPath(string $strPath) : array
232
    {
233
        $aSplit = explode($this->strSeparator, $strPath);
234
        if ($aSplit === false) {
235
            $aSplit = [$strPath];
236
        }
237
        return $aSplit;
238
    }
239
240
    /**
241
     * Convert string to bool.
242
     * Accepted values are (case insensitiv): <ul>
243
     * <li> true, on, yes, 1 </li>
244
     * <li> false, off, no, none, 0 </li></ul>
245
     * for all other values the default value is returned!
246
     * @param string $strValue
247
     * @param bool $bDefault
248
     * @return bool
249
     */
250
    protected function boolFromString(string $strValue, bool $bDefault = false) : bool
251
    {
252
        if ($this->isTrue($strValue)) {
253
            return true;
254
        } else if ($this->isFalse($strValue)) {
255
            return false;
256
        } else {
257
            return $bDefault;
258
        }
259
    }
260
261
    /**
262
     * Checks whether the passed value is a valid expression for bool 'True'.
263
     * Accepted values for bool 'true' are (case insensitiv): <i>true, on, yes, 1</i>
264
     * @param string $strValue
265
     * @return bool
266
     */
267
    protected function isTrue(string $strValue) : bool
268
    {
269
        $strValue = strtolower($strValue);
270
        return in_array($strValue, ['true', 'on', 'yes', '1']);
271
    }
272
273
    /**
274
     * Checks whether the passed value is a valid expression for bool 'False'.
275
     * Accepted values for bool 'false' are (case insensitiv): <i>false, off, no, none, 0</i>
276
     * @param string $strValue
277
     * @return bool
278
     */
279
    protected function isFalse(string $strValue) : bool
280
    {
281
        $strValue = strtolower($strValue);
282
        return in_array($strValue, ['false', 'off', 'no', 'none', '0']);
283
    }
284
285
    /**
286
     * Merge this instance with values from onather config.
287
     * Note that the elemenst of the config to merge with has allways higher priority than
288
     * the elements of this instance. <br/>
289
     * If both config contains elements with the same key, the value of this instance will be
290
     * replaced with the value of the config we merge with. <br/>
291
     * <b>So keep allways the order in wich you merge several configs together in mind.</b>
292
     * @param AbstractConfig $oMerge
293
     */
294
    public function mergeWith(AbstractConfig $oMerge) : void
295
    {
296
        $aMerge = $oMerge->getConfig();
297
        if ($this->aConfig === null) {
298
            $this->aConfig = $aMerge;
299
            return;
300
        }
301
        $this->aConfig = $this->mergeArrays($this->aConfig, $aMerge);
302
    }
303
304
    /**
305
     * Merge the values of two array into one resulting array.
306
     * <b>Note: <i>neither array_merge() nor array_merge_recursive() lead to
307
     * the desired result</i></b><br/><br/>
308
     * Assuming following two config:<pre>
309
     *      $a1 = ["a" => ["c1" => "red", "c2" => "green"]];
310
     *      $a2 = ["a" => ["c2" => "blue", "c3" => "yellow"]]; </pre>
311
     * We expect as result for merge($a1, $a2): <pre>
312
     *      $a3 = ["a" => ["c1" => "red", "c2" => "blue", "c3" => "yellow"]]; </pre>
313
     * => [a][c1] remains on "red", [a][c2] "green" is replaced by "blue" and [a][c3] is supplemented with "yellow" <br/><br/>
314
     * But <ol>
315
     * <li><b>$a3 = array_merge($a1, $a2)</b> will result in: <pre>
316
     *      $a3 = ["a" => ["c2" => "blue", "c3" => "yellow"]]; </pre>
317
     * => the entire element [a] is replaced by the content of $a2 - the sub-elements
318
     * of $a1 that are not contained in $a2 are lost! <br/><br/>
319
     * </li>
320
     * <li><b>$a3 = array_merge_recursive($a1, $a2)</b> will result in: <pre>
321
     *      $a3 = ["a" => ["c1" => red, "c2" => ["green", "blue"], "c3" => "yellow"]]</pre>
322
     * => [a][c2] changes from string to an array ["green", "blue"]!
323
     * </li></ol>
324
     * @param array<mixed> $aBase
325
     * @param array<mixed> $aMerge
326
     * @return array<mixed>
327
     */
328
    protected function mergeArrays(array $aBase, array $aMerge) : array
329
    {
330
        foreach ($aMerge as $keyMerge => $valueMerge) {
331
            if (isset($aBase[$keyMerge]) && is_array($aBase[$keyMerge]) && is_array($valueMerge)) {
332
                // The element is available in the basic configuration and both elements contains
333
                // an array
334
                // -> call mergeArray () recursively, unless it is a zero index based array in both cases
335
                if ($this->isAssoc($aBase[$keyMerge]) || $this->isAssoc($valueMerge)) {
336
                    $aBase[$keyMerge] = $this->mergeArrays($aBase[$keyMerge], $valueMerge);
337
                    continue;
338
                }
339
            }
340
            // in all other cases either the element from the array that is to be merged is inserted
341
            // or it has priority over the original element
342
            $aBase[$keyMerge] = $valueMerge;
343
        }
344
        return $aBase;
345
    }
346
347
    /**
348
     * Check if given array is associative.
349
     * Only if the array exactly has sequential numeric keys, starting from 0, the
350
     * array is NOT associative.
351
     * @param array<mixed> $a
352
     * @return bool
353
     */
354
    protected function isAssoc(array $a) : bool
355
    {
356
        if ($a === []) {
357
            return false;
358
        }
359
        return array_keys($a) !== range(0, count($a) - 1);
360
    }
361
}