GLConf::processConfig()   A
last analyzed

Complexity

Conditions 5
Paths 3

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 5

Importance

Changes 3
Bugs 1 Features 0
Metric Value
eloc 6
c 3
b 1
f 0
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 9.6111
cc 5
nc 3
nop 1
crap 5
1
<?php
2
3
namespace GeekLab\Conf;
4
5
use GeekLab\Conf\Driver\ConfDriverInterface;
6
7
final class GLConf
8
{
9
    protected array $configuration = [];
10
    private ConfDriverInterface $driver;
11
    private array $injectedValues;
12
    private array $options;
13
14
    /**
15
     * GLConf constructor.
16
     * Inject our driver (strategy) here.
17
     *
18
     * @param ConfDriverInterface $driver
19
     * @param array               $valueInjections
20
     * @param array               $options
21
     */
22 8
    public function __construct(
23
        ConfDriverInterface $driver,
24
        array $valueInjections = [],
25
        array $options = ['keys_upper_case']
26
    ) {
27 8
        $this->driver = $driver;
28 8
        $this->injectedValues = $valueInjections;
29 8
        $this->options = $options;
30
    }
31
32
    /**
33
     * Get data by dot notation.
34
     * Stolen from: https://stackoverflow.com/a/14706302/344028
35
     *
36
     * @param string $key dot notated array key accessor.
37
     *
38
     * @return mixed
39
     */
40 8
    public function get(string $key): mixed
41
    {
42 8
        if (in_array('keys_lower_case', $this->options, true)) {
43
            // Convert key to lower case.
44 1
            $key = strtolower($key);
45 7
        } elseif (!in_array('keys_same_case', $this->options, true)) {
46
            // Convert key to upper case.
47 6
            $key = strtoupper($key);
48
        }
49
50
        /** @var mixed $config Save configuration for local scope modification. */
51 8
        $config = $this->configuration;
52
53
        /** @var array<string> | bool $token Tokenize the key to do iterations over the config with. */
54 8
        $token = strtok($key, '.');
55
56
        // Loop until we are out of tokens.
57 8
        while ($token !== false) {
58 8
            if (!isset($config[$token])) {
59
                // Array key of $token wasn't found.
60 8
                return false;
61
            }
62
63
            // Save the data found.
64 8
            $config = $config[$token];
65
66
            // Advanced to the next token, or set token to false if nothing else if left..
67 8
            $token = strtok('.');
68
        }
69
70
        // Return the valid found by the previous loop.
71 8
        return $config;
72
    }
73
74
    /**
75
     * Return the compiled configuration.
76
     *
77
     * @return array
78
     */
79 4
    public function getAll(): array
80
    {
81 4
        return $this->configuration;
82
    }
83
84
    /**
85
     * Initialize the configuration system.
86
     */
87 8
    public function init(): void
88
    {
89 8
        $confKey = 'conf';
90 8
        if (in_array('keys_upper_case', $this->options, true)) {
91 6
            $confKey = 'CONF';
92
        }
93
94
        // Load main (top level) configuration.
95 8
        $this->configuration = $this->driver->parseConfigurationFile();
96
97
        // Overload configuration with injected values.
98 8
        $this->configuration = array_merge($this->configuration, $this->injectedValues);
99
100
        // Conform the array (cases and changes spaces to underscore characters in keys).
101 8
        $this->configuration = $this->conformArray($this->configuration);
102 8
        $config = [];
103
104
        // Load in the extra configuration via the CONF property.
105 8
        if (isset($this->configuration[$confKey]) && is_array($this->configuration[$confKey])) {
106 8
            foreach ($this->configuration[$confKey] as $file) {
107
                // Load in the referenced configuration from the main configuration.
108 8
                $innerConfig = $this->driver->parseConfigurationFile($file);
109
110
                // Conform the configuration array.
111 8
                $innerConfig = $this->conformArray($innerConfig);
112
113
                // Strip out anything that wasn't in a section (non-array value at the top level).
114
                // We don't want the ability to overwrite stuff from main configuration file.
115 8
                foreach ($innerConfig as $k => $v) {
116 8
                    if (!is_array($v)) {
117 8
                        unset($innerConfig[$k]);
118
                    }
119
                }
120
121
                // Store conformed configuration into temporary array for merging later.
122 8
                $config[] = $innerConfig;
123
            }
124
125
            // Combine/Merge/Overwrite compiled configuration with current.
126
            // Uses the splat operator on the arrays stored in the temporary config.
127 8
            $this->configuration = array_replace_recursive($this->configuration, ...$config) ?: [];
128
        }
129
130
        // Fill in the placeholders.
131 8
        $this->configuration = $this->processConfig($this->configuration);
132
    }
133
134
    /**
135
     * Make the array conform to some sort of standard.
136
     * -  Convert key names cases (maybe).
137
     * -  Convert spaces and periods to underscores.
138
     *
139
     * @param array $arr
140
     *
141
     * @return array
142
     */
143 8
    protected function conformArray(array $arr): array
144
    {
145
        // Store our conformed array for returning.
146 8
        $fixed = [];
147
148 8
        if (in_array('keys_lower_case', $this->options, true)) {
149
            // Convert keys to lower case.
150 1
            $arr = array_change_key_case($arr);
151 7
        } elseif (!in_array('keys_same_case', $this->options, true)) {
152
            // Convert keys to upper case.
153 6
            $arr = array_change_key_case($arr, CASE_UPPER);
154
        }
155
156 8
        foreach ($arr as $k => $v) {
157
            // Recursively conform the inner arrays.
158 8
            if (is_array($v)) {
159 8
                $v = $this->conformArray($v);
160
            }
161
162
            // Replace spaces and periods with underscores.
163 8
            $fixed[preg_replace('/\s+|\.+/', '_', $k)] = $v;
164
        }
165
166
        // Return the conformed array.
167 8
        return $fixed;
168
    }
169
170
    /**
171
     * Self referenced and environment variable placeholder replacement.
172
     *
173
     * @param string $value
174
     *
175
     * @return string
176
     */
177 8
    private function fillPlaceHolders(string $value): string
178
    {
179
        // Certain recursive stuff, like @[SelfReferencedPlaceholder.@[SomeStuff.a]] is what triggers this part.
180
        // Find the self referenced placeholders and fill them.
181
        // Force the type to string, in possible case of null.
182 8
        $data = (string) preg_replace_callback(
183
            '/@\[([a-zA-Z0-9_.-]*?)]/',
184 8
            function ($matches): string {
185
                // Does this key exist, is so fill this match, if not, just return the match intact.
186 8
                return $this->get($matches[1]) ?: $matches[0];
187
            },
188
            $value
189
        );
190
191
        // Find the recursive self referenced placeholders and fill them.
192 8
        if ($data !== $value && preg_match('/@\[([a-zA-Z0-9_.-]*?)]/', $data)) {
193 7
            $data = (string) $this->processConfig($data);
194
        }
195
196
        // Find the environment variable placeholders and fill them.
197
        // Force the type to string, in possible case of null.
198 8
        return (string) preg_replace_callback(
199
            '/\$\[([a-zA-Z0-9_.-]*?)]/',
200 8
            static function ($matches) {
201
                // Replace with local variable (non-SAPI)
202
                // Or keep intact if one isn't found.
203 8
                return !empty(getenv($matches[1], true)) ? getenv($matches[1], true) : $matches[0];
204
            },
205
            $data
206
        );
207
    }
208
209
    /**
210
     * Run through the configuration and process the placeholders.
211
     *
212
     * @param mixed $data
213
     *
214
     * @return mixed
215
     */
216 8
    private function processConfig(mixed $data): mixed
217
    {
218 8
        if (is_array($data)) {
219
            // It's an array, so let's loop through it.
220 8
            foreach ($data as $k => $val) {
221 8
                $data[$k] = is_string($val) ? $this->fillPlaceHolders($val) : $this->processConfig($val);
222
            }
223 7
        } elseif (is_string($data)) {
224 7
            $data = $this->fillPlaceHolders($data);
225
        }
226
227 8
        return $data;
228
    }
229
}
230