Config::getConfig()   A
last analyzed

Complexity

Conditions 4
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
/**
4
 * \AppserverIo\Doppelgaenger\Config
5
 *
6
 * NOTICE OF LICENSE
7
 *
8
 * This source file is subject to the Open Software License (OSL 3.0)
9
 * that is available through the world-wide-web at this URL:
10
 * http://opensource.org/licenses/osl-3.0.php
11
 *
12
 * PHP version 5
13
 *
14
 * @author    Bernhard Wick <[email protected]>
15
 * @copyright 2015 TechDivision GmbH - <[email protected]>
16
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
17
 * @link      https://github.com/appserver-io/doppelgaenger
18
 * @link      http://www.appserver.io/
19
 */
20
21
namespace AppserverIo\Doppelgaenger;
22
23
use AppserverIo\Doppelgaenger\Interfaces\ConfigInterface;
24
use AppserverIo\Doppelgaenger\Exceptions\ConfigException;
25
use AppserverIo\Doppelgaenger\Utils\Formatting;
26
use AppserverIo\Doppelgaenger\Utils\InstanceContainer;
27
use AppserverIo\Doppelgaenger\Dictionaries\ReservedKeywords;
28
29
/**
30
 * This class implements the access point for our global (oh no!) configuration
31
 *
32
 * @author    Bernhard Wick <[email protected]>
33
 * @copyright 2015 TechDivision GmbH - <[email protected]>
34
 * @license   http://opensource.org/licenses/osl-3.0.php Open Software License (OSL 3.0)
35
 * @link      https://github.com/appserver-io/doppelgaenger
36
 * @link      http://www.appserver.io/
37
 */
38
class Config implements ConfigInterface
39
{
40
    /**
41
     * @const string DEFAULT_CONFIG Name of the default configuration file
42
     */
43
    const DEFAULT_CONFIG = 'config.default.json';
44
45
    /**
46
     * The delimiter for values names as they are used externally
47
     *
48
     * @const string VALUE_NAME_DELIMITER
49
     */
50
    const VALUE_NAME_DELIMITER = '/';
51
52
    /**
53
     * @var string $context The context for this instance e.g. app based configurations
54
     */
55
    protected $context;
56
57
    /**
58
     * @var array $config Configuration array
59
     */
60
    protected $config = array();
61
62
    /**
63
     * Default constructor
64
     */
65
    public function __construct()
66
    {
67
        $this->load(__DIR__ . DIRECTORY_SEPARATOR . self::DEFAULT_CONFIG);
68
    }
69
70
    /**
71
     * Will flatten a multi-level associative array into a one-level one
72
     *
73
     * @param array  $array       The array to flatten
74
     * @param string $parentKey   The key of the parent array, used within recursion
75
     * @param bool   $initialCall Is this the initial call or recursion?
76
     *
77
     * @return array
78
     */
79
    protected function flattenArray(array $array, $parentKey = '', $initialCall = true)
80
    {
81
        $result = array();
82
        foreach ($array as $key => $value) {
83
            // If it is an array not containing integer keys (so no nested config element) we have to get recursive
84
            if (is_array($value) && @!is_int(array_keys($value)[0])) {
85
                $value = $this->flattenArray($value, $key, false);
86
            }
87
88
            // Save the result with a newly combined key
89
            $result[trim($parentKey . self::VALUE_NAME_DELIMITER . $key, self::VALUE_NAME_DELIMITER)] = $value;
90
        }
91
92
        // If we are within the initial call we would like to do a final flattening and sorting process
93
        if ($initialCall === true) {
94
            // No iterate all the entries and array shift them if they are arrays
95
            foreach ($result as $key => $value) {
96
                if (is_array($value)) {
97
                    unset($result[$key]);
98
                    $result = array_merge($result, $value);
99
                }
100
            }
101
102
            // Sort the whole thing as we might have mixed it up little
103
            ksort($result);
104
        }
105
        return $result;
106
    }
107
108
    /**
109
     * Sets a value to specific content
110
     *
111
     * @param string $valueName The value to set
112
     * @param mixed  $value     The actual content for the value
113
     *
114
     * @return void
115
     */
116
    public function setValue($valueName, $value)
117
    {
118
        // Set the value
119
        $this->config[$valueName] = $value;
120
    }
121
122
    /**
123
     * Might be called after the all config data has been loaded.
124
     * Will store all config object instances which might be needed later in the instance container util.
125
     *
126
     * @return void
127
     */
128
    public function storeInstances()
129
    {
130
        $instanceContainer = new InstanceContainer();
131
132
        // One thing we need is the logger instance (if any)
133
        if ($this->hasValue('enforcement/logger')) {
134
            $logger = $this->extractLoggerInstance($this->config);
135
136
            if ($logger !== false) {
137
                $instanceContainer[ReservedKeywords::LOGGER_CONTAINER_ENTRY] = $logger;
138
            }
139
        }
140
    }
141
142
    /**
143
     * Extends a value by specific content. If the value is an array it will be merged, otherwise it will be
144
     * string-concatinated to the end of the current value
145
     *
146
     * @param string $valueName The value to extend
147
     * @param string $value     The actual content for the value we want to add to the original
148
     *
149
     * @return void
150
     */
151
    public function extendValue($valueName, $value)
152
    {
153
        // Get the original value
154
        $originalValue = $this->getValue($valueName);
155
156
        // If we got an array
157
        if (is_array($value)) {
158
            if (is_array($originalValue)) {
159
                $newValue = array_merge($originalValue, $value);
160
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
161
            } else {
162
                $newValue = array_merge(array($originalValue), $value);
163
            }
164
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
165
        } else {
166
            $newValue = $originalValue . $value;
167
        }
168
169
        // Finally set the new value
170
        $this->setValue($valueName, $newValue);
171
    }
172
173
    /**
174
     * Will extract an logger instance from the configuration array.
175
     * Returns false on error.
176
     *
177
     * @param array $configArray The config to extract the logger instance from
178
     *
179
     * @return object|boolean
180
     */
181
    protected function extractLoggerInstance(array $configArray)
182
    {
183
        if (isset($configArray['enforcement/logger'])) {
184
            // Get the logger
185
            $logger = $configArray['enforcement/logger'];
186
            if (is_string($logger)) {
187
                $logger = new $logger;
188
            }
189
190
            // Return the logger
191
            return $logger;
192
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
193
        } else {
194
            return false;
195
        }
196
    }
197
198
    /**
199
     * Unsets a specific config value
200
     *
201
     * @param string $value The value to unset
202
     *
203
     * @return void
204
     */
205
    public function unsetValue($value)
206
    {
207
        if (isset($this->config[$value])) {
208
            // unset the value
209
            unset($this->config[$value]);
210
        }
211
    }
212
213
    /**
214
     * Returns the content of a specific config value
215
     *
216
     * @param string $value The value to get the content for
217
     *
218
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ConfigException
219
     *
220
     * @return mixed
221
     */
222
    public function getValue($value)
223
    {
224
        // check if server var is set
225
        if (isset($this->config[$value])) {
226
            // return server vars value
227
            return $this->config[$value];
228
        }
229
        // throw exception
230
        throw new ConfigException(sprintf("Config value %s does not exist.", $value));
231
    }
232
233
    /**
234
     * Checks if value exists for given value
235
     *
236
     * @param string $value The value to check
237
     *
238
     * @return boolean Weather it has value (true) or not (false)
239
     */
240
    public function hasValue($value)
241
    {
242
        // check if server var is set
243
        if (!isset($this->config[$value])) {
244
            return false;
245
        }
246
247
        return true;
248
    }
249
250
    /**
251
     * Will load a certain configuration file into this instance. Might throw an exception if the file is not valid
252
     *
253
     * @param string $file The path of the configuration file we should load
254
     *
255
     * @return \AppserverIo\Doppelgaenger\Config
256
     *
257
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ConfigException
258
     */
259
    public function load($file)
260
    {
261
        // Do we load a valid config?
262
        $configCandidate = $this->validate($file);
263
        if ($configCandidate === false) {
264
            throw new ConfigException(sprintf('Attempt to load invalid configuration file %s', $file));
265
        }
266
267
        $this->config = array_replace_recursive($this->config, $configCandidate);
268
269
        return $this;
270
    }
271
272
    /**
273
     * Will validate a potential configuration file. Returns false if file is no valid Doppelgaenger configuration, true otherwise
274
     *
275
     * @param string $file Path of the potential configuration file
276
     *
277
     * @return boolean
278
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ConfigException
279
     */
280
    public function isValidConfigFile($file)
281
    {
282
        return is_array($this->validate($file));
283
    }
284
285
    /**
286
     * Will normalize directories mentioned within a configuration aspect.
287
     * If there is an error false will be returned. If not we will return the given configuration array containing only
288
     * normalized paths.
289
     *
290
     * @param string $configAspect The aspect to check for non-normal dirs
291
     * @param array  $configArray  The array to check within
292
     *
293
     * @return array|bool
294
     */
295
    protected function normalizeConfigDirs($configAspect, array $configArray)
296
    {
297
        // Are there dirs within this config aspect?
298
        if (isset($configArray[$configAspect . self::VALUE_NAME_DELIMITER . 'dirs'])) {
299
            // Get ourselves a format utility
300
            $formattingUtil = new Formatting();
301
302
            // Iterate over all dir entries and normalize the paths
303
            foreach ($configArray[$configAspect . self::VALUE_NAME_DELIMITER . 'dirs'] as $key => $projectDir) {
304
                // Do the normalization
305
                $tmp = $formattingUtil->sanitizeSeparators($formattingUtil->normalizePath($projectDir));
306
307
                if (is_readable($tmp)) {
308
                    $configArray[$configAspect . self::VALUE_NAME_DELIMITER . 'dirs'][$key] = $tmp;
309
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
310
                } elseif (preg_match('/\[|\]|\*|\+|\.|\(|\)|\?|\^/', $tmp)) {
311
                    // Kill the original path entry so the iterators wont give us a bad time
312
                    unset($configArray[$configAspect . self::VALUE_NAME_DELIMITER . 'dirs'][$key]);
313
314
                    // We will open up the paths with glob
315
                    foreach (glob($tmp, GLOB_ERR) as $regexlessPath) {
316
                        // collect the cleaned path
317
                        $configArray[$configAspect . self::VALUE_NAME_DELIMITER . 'dirs'][] = $regexlessPath;
318
                    }
319
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
320
                } else {
321
                    // Somethings wrong with the path, that should not be
322
                    return false;
323
                }
324
            }
325
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
326
        }
327
328
        // Everything seems fine, lets return the changes config array
329
        return $configArray;
330
    }
331
332
    /**
333
     * Will return the whole configuration or, if $aspect is given, certain parts of it
334
     *
335
     * @param string $aspect The aspect of the configuration we are interested in e.g. 'autoloader'
336
     *
337
     * @return array
338
     */
339
    public function getConfig($aspect = null)
340
    {
341
        if (!is_null($aspect)) {
342
            // Filter the aspect our of the config
343
            $tmp = array();
344
            foreach ($this->config as $key => $value) {
345
                // Do we have an entry belonging to the certain aspect? If so filter it and cut the aspect key part
346
                if (strpos($key, $aspect . self::VALUE_NAME_DELIMITER) === 0) {
347
                    $tmp[str_replace($aspect . self::VALUE_NAME_DELIMITER, '', $key)] = $value;
348
                }
349
            }
350
351
            return $tmp;
352
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
353
        } else {
354
            // Just return the whole config
355
356
            return $this->config;
357
        }
358
    }
359
360
    /**
361
     * Will validate a potential configuration file. Returns false if file is no valid Doppelgaenger configuration.
362
     * Will return the validated configuration on success
363
     *
364
     * @param string $file Path of the potential configuration file
365
     *
366
     * @return array|boolean
367
     * @throws \AppserverIo\Doppelgaenger\Exceptions\ConfigException
368
     */
369
    protected function validate($file)
370
    {
371
        $configCandidate = json_decode(file_get_contents($file), true);
372
373
        // Did we even get an array?
374
        if (!is_array($configCandidate)) {
375
            throw new ConfigException(sprintf('Could not parse configuration file %s.', $file));
376
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
377
        } else {
378
            $configCandidate = $this->flattenArray($configCandidate);
379
        }
380
381
        // We need some formatting utilities
382
        $formattingUtil = new Formatting();
383
384
        // We will normalize the paths we got and check if they are valid
385
        if (isset($configCandidate['cache' . self::VALUE_NAME_DELIMITER . 'dir'])) {
386
            $tmp = $formattingUtil->normalizePath($configCandidate['cache' . self::VALUE_NAME_DELIMITER . 'dir']);
387
388
            if (is_writable($tmp)) {
389
                $configCandidate['cache' . self::VALUE_NAME_DELIMITER . 'dir'] = $tmp;
390
0 ignored issues
show
Coding Style introduced by
Blank line found at end of control structure
Loading history...
391
            } else {
392
                throw new ConfigException(sprintf('The configured cache directory %s is not writable.', $tmp));
393
            }
394
        }
395
396
        // Same for enforcement dirs
397
        $configCandidate = $this->normalizeConfigDirs('enforcement', $configCandidate);
398
399
        // Do we still have an array here?
400
        if (!is_array($configCandidate)) {
401
            return false;
402
        }
403
404
        // Do the same for the autoloader dirs
405
        $configCandidate = $this->normalizeConfigDirs('autoloader', $configCandidate);
406
407
        // Lets check if there is a valid processing in place
408
        if ($configCandidate === false || !$this->validateProcessing($configCandidate)) {
409
            return false;
410
        }
411
412
        // Return what we got
413
        return $configCandidate;
414
    }
415
416
    /**
417
     * Will return true if the processing part of the config candidate array is valid. Will return false if not
418
     *
419
     * @param array $configCandidate The config candidate we want to validate in terms of processing
420
     *
421
     * @return boolean
422
     *
423
     * @todo move everything other than logger check to JSON scheme validation
0 ignored issues
show
Coding Style introduced by
Comment refers to a TODO task

This check looks TODO comments that have been left in the code.

``TODO``s show that something is left unfinished and should be attended to.

Loading history...
424
     */
425
    protected function validateProcessing(array $configCandidate)
426
    {
427
        // Merge it with the current config as a standalone config might not be valid at all
428
        $configCandidate = array_replace_recursive($this->config, $configCandidate);
429
430
        $validEntries = array_flip(array('none', 'exception', 'logging'));
431
432
        // Do we have an entry at all?
433
        if (!isset($configCandidate['enforcement/processing'])) {
434
            return false;
435
        }
436
437
        // Did we even get something useful?
438
        if (!isset($validEntries[$configCandidate['enforcement/processing']])) {
439
            return false;
440
        }
441
442
        // If we got the option "logger" we have to check if there is a logger. If not, we fail, if yes
443
        // we have to check if we got something PSR-3 compatible
444
        if ($configCandidate['enforcement/processing'] === 'logging') {
445
            return $this->extractLoggerInstance($configCandidate) instanceof \Psr\Log\LoggerInterface;
446
        }
447
448
        // Still here? Sounds good
449
        return true;
450
    }
451
}
452