Passed
Push — master ( 9d4b70...966dd2 )
by Marwan
04:50 queued 03:21
created

Config::set()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 2
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 5
ccs 3
cts 3
cp 1
crap 1
rs 10
1
<?php
2
3
/**
4
 * @author Marwan Al-Soltany <[email protected]>
5
 * @copyright Marwan Al-Soltany 2021
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
declare(strict_types=1);
11
12
namespace MAKS\Velox\Backend;
13
14
use MAKS\Velox\App;
15
use MAKS\Velox\Backend\Event;
16
use MAKS\Velox\Helper\Misc;
17
18
/**
19
 * A class that loads everything from the "/config" directory and make it as an array that is accessible via dot-notation.
20
 *
21
 * Example:
22
 * ```
23
 * // get the entire config
24
 * $entireConfig = Config::getAll();
25
 *
26
 * // check for config value availability
27
 * $varNameExists = Config::has('filename.config.varName');
28
 *
29
 * // get a specific config value or fall back to a default value
30
 * $varName = Config::get('filename.config.varName', 'fallbackValue');
31
 *
32
 * // set a specific config value at runtime
33
 * Config::set('filename.config.varName', 'varValue');
34
 *
35
 * // delete cached config
36
 * Config::clearCache();
37
 * ```
38
 *
39
 * @since 1.0.0
40
 * @api
41
 */
42
class Config
43
{
44
    /**
45
     * This event will be dispatched when the config is loaded.
46
     * This event will be passed a reference to the config array.
47
     *
48
     * @var string
49
     */
50
    public const ON_LOAD = 'config.on.load';
51
52
    /**
53
     * This event will be dispatched when the config is cached.
54
     * This event will not be passed any arguments.
55
     *
56
     * @var string
57
     */
58
    public const ON_CACHE = 'config.on.cache';
59
60
    /**
61
     * This event will be dispatched when the config cache is cleared.
62
     * This event will not be passed any arguments.
63
     *
64
     * @var string
65
     */
66
    public const ON_CLEAR_CACHE = 'config.on.clearCache';
67
68
69
    /**
70
     * The default directory of the configuration files.
71
     *
72
     * @var string
73
     */
74
    public const CONFIG_DIR = BASE_PATH . '/config';
75
76
    /**
77
     * The path of the cached configuration file.
78
     *
79
     * @var string
80
     */
81
    public const CONFIG_CACHE_FILE = BASE_PATH . '/storage/cache/config/config.json';
82
83
84
    /**
85
     * The currently loaded configuration.
86
     */
87
    protected static array $config;
88
89
90 19
    public function __construct()
91
    {
92 19
        $this->load();
93 19
    }
94
95 1
    public function __toString()
96
    {
97 1
        return static::CONFIG_DIR;
98
    }
99
100
101
    /**
102
     * Includes all files in a directory.
103
     *
104
     * @param string $path
105
     *
106
     * @return array
107
     */
108 6
    private static function include(string $path): array
109
    {
110 6
        $includes = [];
111 6
        $include  = static function ($file) {
112 6
            if (is_file($file)) {
113 6
                $info = pathinfo($file);
114 6
                return $info['extension'] == 'php' ? include($file) : [];
115
            }
116
117 1
            return self::include($file);
118 6
        };
119
120
        // load all config files
121 6
        $filenames = scandir($path) ?: [];
122 6
        foreach ($filenames as $filename) {
123 6
            $file = sprintf('%s/%s', $path, $filename);
124 6
            $config = basename($filename, '.php');
125
126
            // do not include items that have dots in their names
127
            // as this will conflict with array access separator
128 6
            if (strpos($config, '.') !== false) {
129 6
                continue;
130
            }
131
132 6
            $includes[$config] = isset($includes[$config])
133
                ? $include($file) + (array)$includes[$config]
134 6
                : $include($file);
135
        }
136
137 6
        return $includes;
138
    }
139
140
    /**
141
     * Parses the configuration to replace reference of some "{filename.config.varName}" with actual value from the passed configuration.
142
     *
143
     * @param array $config
144
     *
145
     * @return array
146
     */
147 6
    private static function parse(array $config): array
148
    {
149
        // parses all config variables
150 6
        $tries = count($config);
151 6
        for ($i = 0; $i < $tries; $i++) {
152 6
            array_walk_recursive($config, function (&$value) use (&$config) {
153 6
                if (is_string($value)) {
154 6
                    if (preg_match_all('/{([a-z0-9_\-\.]*)}/i', $value, $matches)) {
155 6
                        $variables = [];
156 6
                        array_walk($matches[1], function (&$variable) use (&$variables, &$config) {
157 6
                            $variables[$variable] = Misc::getArrayValueByKey($config, $variable, null);
158 6
                        });
159
160 6
                        $value = $value === $matches[0][0]
161 6
                            ? $variables[$matches[1][0]]
162 6
                            : Misc::interpolate($value, $variables);
163
                    }
164
                }
165 6
            });
166
        }
167
168 6
        return $config;
169
    }
170
171
    /**
172
     * Loads the configuration (directly or when available from cache) and sets class internal state.
173
     *
174
     * @return void
175
     */
176 83
    protected static function load(): void
177
    {
178 83
        $configDir       = static::CONFIG_DIR;
179 83
        $configCacheFile = static::CONFIG_CACHE_FILE;
180
181 83
        if (empty(static::$config)) {
182 6
            if (file_exists($configCacheFile)) {
183 1
                $configJson     = file_get_contents($configCacheFile);
184 1
                static::$config = json_decode($configJson, true);
185
186 1
                return;
187
            }
188
189 6
            static::$config = self::parse(self::include($configDir));
190
191 6
            Event::dispatch(self::ON_LOAD, [&static::$config]);
192
        }
193 83
    }
194
195
    /**
196
     * Caches the current configuration as JSON. Note that a new version will not be generated unless the cache is cleared.
197
     *
198
     * @return void
199
     */
200 3
    public static function cache(): void
201
    {
202 3
        $configDir       = static::CONFIG_DIR;
203 3
        $configCacheFile = static::CONFIG_CACHE_FILE;
204 3
        $configCacheDir  = dirname($configCacheFile);
205
206 3
        if (file_exists($configCacheFile)) {
207 1
            return;
208
        }
209
210 2
        if (!file_exists($configCacheDir)) {
211 2
            mkdir($configCacheDir, 0744, true);
212
        }
213
214 2
        $config     = self::parse(self::include($configDir));
215 2
        $configJson = json_encode($config, JSON_PRETTY_PRINT);
216
217 2
        file_put_contents($configCacheFile, $configJson, LOCK_EX);
218
219 2
        Event::dispatch(self::ON_CACHE);
220
221 2
        App::log(
222 2
            'Generated cache for system config, checksum (SHA-256: {checksum})',
223 2
            ['checksum' => hash('sha256', $configJson)],
224 2
            'system'
225
        );
226 2
    }
227
228
    /**
229
     * Deletes the cached configuration JSON and resets class internal state.
230
     *
231
     * @return void
232
     */
233 2
    public static function clearCache(): void
234
    {
235 2
        static::$config = [];
236
237 2
        $configCacheFile = static::CONFIG_CACHE_FILE;
238
239 2
        if (file_exists($configCacheFile)) {
240 1
            unlink($configCacheFile);
241
        }
242
243 2
        Event::dispatch(self::ON_CLEAR_CACHE);
244
245 2
        App::log('Cleared config cache', null, 'system');
246 2
    }
247
248
    /**
249
     * Checks whether a value of a key exists in the configuration via dot-notation.
250
     *
251
     * @param string $key The dotted key representation.
252
     *
253
     * @return bool
254
     */
255 1
    public static function has(string $key): bool
256
    {
257 1
        static::load();
258
259 1
        $value = Misc::getArrayValueByKey(static::$config, $key, null);
260
261 1
        return isset($value);
262
    }
263
264
    /**
265
     * Gets a value of a key from the configuration via dot-notation.
266
     *
267
     * @param string $key The dotted key representation.
268
     * @param mixed $fallback [optional] The default fallback value.
269
     *
270
     * @return mixed The requested value or null.
271
     */
272 48
    public static function get(string $key, $fallback = null)
273
    {
274 48
        static::load();
275
276 48
        return Misc::getArrayValueByKey(static::$config, $key, $fallback);
277
    }
278
279
    /**
280
     * Sets a value of a key in the configuration via dot-notation.
281
     *
282
     * @param string $key The dotted key representation.
283
     * @param mixed $value The value to set.
284
     *
285
     * @return void
286
     */
287 44
    public static function set(string $key, $value): void
288
    {
289 44
        static::load();
290
291 44
        Misc::setArrayValueByKey(static::$config, $key, $value);
292 44
    }
293
294
    /**
295
     * Returns the currently loaded configuration.
296
     *
297
     * @return array
298
     */
299 3
    public static function getAll(): ?array
300
    {
301 3
        static::load();
302
303 3
        return static::$config;
304
    }
305
}
306