Passed
Push — master ( 26c234...c8a6d1 )
by Marwan
09:32
created

Config::getReference()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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