Passed
Push — master ( f5c6b6...ee8c1e )
by Kenneth
01:54
created

GLConf::fillPlaceHolders()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 39
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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