Completed
Pull Request — master (#42)
by Vladimir
02:39
created

Configuration::defaultConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 0
dl 0
loc 23
ccs 8
cts 8
cp 1
crap 1
rs 9.0856
c 0
b 0
f 0

2 Methods

Rating   Name   Duplication   Size   Complexity  
A Configuration::getRedirectTemplate() 0 4 1
A Configuration::returnConfigOption() 0 4 2
1
<?php
2
3
/**
4
 * @copyright 2017 Vladimir Jimenez
5
 * @license   https://github.com/allejo/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx;
9
10
use allejo\stakx\Exception\FileAccessDeniedException;
11
use allejo\stakx\Exception\RecursiveConfigurationException;
12
use allejo\stakx\System\Filesystem;
13
use allejo\stakx\Utilities\ArrayUtilities;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
17
use Symfony\Component\Yaml\Exception\ParseException;
18
use Symfony\Component\Yaml\Yaml;
19
20
class Configuration implements LoggerAwareInterface
21
{
22
    const DEFAULT_NAME = '_config.yml';
23
    const IMPORT_KEYWORD = 'import';
24
25
    private static $configImports = array();
26
27
    /**
28
     * A list of regular expressions or files directly related to stakx websites that should not be copied over to the
29
     * compiled website as an asset.
30
     *
31
     * @var array
32
     */
33
    public static $stakxSourceFiles = array('/^_(?!themes).*/', '/.twig$/');
34
35
    /**
36
     * An array representation of the main Yaml configuration.
37
     *
38
     * @var array
39
     */
40
    private $configuration;
41
42
    /**
43
     * @var string
44
     */
45
    private $parentConfig;
46
47
    /** @var string */
48
    private $currentFile;
49
50
    /**
51
     * @var LoggerInterface
52
     */
53
    private $output;
54
55
    /**
56
     * @var Filesystem
57
     */
58
    private $fs;
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fs. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
59
60
    /**
61
     * Configuration constructor.
62
     */
63 45
    public function __construct()
64
    {
65 45
        $this->configuration = array();
66 45
        $this->fs = new Filesystem();
67 45
    }
68
69
    /**
70
     * {@inheritdoc}
71
     */
72 45
    public function setLogger(LoggerInterface $logger)
73
    {
74 45
        $this->output = $logger;
75 45
    }
76
77
    ///
78
    // Getters
79
    ///
80
81
    /**
82
     * @return bool
83
     */
84 14
    public function isDebug()
85
    {
86 14
        return $this->returnConfigOption('debug', false);
87
    }
88
89
    /**
90
     * @TODO 1.0.0 Remove support for 'base' in next major release; it has been replaced by 'baseurl'
91
     *
92
     * @return mixed|null
93
     */
94 4
    public function getBaseUrl()
95
    {
96 4
        $base = $this->returnConfigOption('base');
97 4
        $baseUrl = $this->returnConfigOption('baseurl');
98
99 4
        if (is_null($base) || (!empty($baseUrl)))
100 4
        {
101 3
            return $baseUrl;
102
        }
103
104 1
        return $base;
105
    }
106
107
    /**
108
     * @return string[]
109
     */
110 1
    public function getDataFolders()
111
    {
112 1
        return $this->returnConfigOption('data');
113
    }
114
115
    /**
116
     * @return string[]
117
     */
118 1
    public function getDataSets()
119
    {
120 1
        return $this->returnConfigOption('datasets');
121
    }
122
123
    /**
124
     * @return string[]
125
     */
126 1
    public function getIncludes()
127
    {
128 1
        return $this->returnConfigOption('include', array());
129
    }
130
131
    /**
132
     * @return string[]
133
     */
134 1
    public function getExcludes()
135
    {
136 1
        return $this->returnConfigOption('exclude', array());
137
    }
138
139
    /**
140
     * @return string
141
     */
142 14
    public function getTheme()
143
    {
144 14
        return $this->returnConfigOption('theme');
145
    }
146
147
    /**
148
     * @return array
149
     */
150 7
    public function getConfiguration()
151
    {
152 7
        return $this->configuration;
153
    }
154
155
    /**
156
     * @return string[]
157
     */
158 1
    public function getPageViewFolders()
159
    {
160 1
        return $this->returnConfigOption('pageviews');
161
    }
162
163
    /**
164
     * @return string
165
     */
166 2
    public function getTargetFolder()
167
    {
168 2
        return $this->returnConfigOption('target');
169
    }
170
171
    /**
172
     * @return string[][]
173
     */
174 1
    public function getCollectionsFolders()
175
    {
176 1
        return $this->returnConfigOption('collections');
177
    }
178
179
    /**
180
     * @return bool
181
     */
182 14
    public function getTwigAutoescape()
0 ignored issues
show
Coding Style introduced by
function getTwigAutoescape() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
183
    {
184 14
        return $this->configuration['twig']['autoescape'];
185
    }
186
187
    /**
188
     * @return false|string
189
     */
190
    public function getRedirectTemplate()
191
    {
192
        return $this->configuration['templates']['redirect'];
193
    }
194
195
    /**
196
     * Return the specified configuration option if available, otherwise return the default.
197
     *
198
     * @param string     $name    The configuration option to lookup
199
     * @param mixed|null $default The default value returned if the configuration option isn't found
200
     *
201
     * @return mixed|null
202
     */
203 45
    private function returnConfigOption($name, $default = null)
204
    {
205 45
        return isset($this->configuration[$name]) ? $this->configuration[$name] : $default;
206
    }
207
208
    ///
209
    // Parsing
210
    ///
211
212
    /**
213
     * Safely read a YAML configuration file and return an array representation of it.
214
     *
215
     * This function will only read files from within the website folder.
216
     *
217
     * @param  string $filePath
218
     *
219
     * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be string|array|\stdClass?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
220
     */
221 32
    private static function readFile($filePath)
222
    {
223 32
        $fs = new Filesystem();
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $fs. Configured minimum length is 3.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
224 32
        $fileRaw = $fs->safeReadFile($filePath);
225 32
        $parsed = Yaml::parse($fileRaw);
226
227 32
        return (null === $parsed) ? array() : $parsed;
228
    }
229
230
    /**
231
     * Parse a configuration file.
232
     *
233
     * @param string|null $configFile
234
     */
235 45
    public function parse($configFile = null)
236
    {
237 45
        $this->parentConfig = $configFile;
238 45
        self::$configImports = array();
239
240 45
        $this->configuration = $this->parseConfig($configFile);
241 45
        $this->mergeDefaultConfiguration();
242 45
        $this->handleDeprecations();
243
244 45
        self::$configImports = array();
245 45
    }
246
247
    /**
248
     * Parse a given configuration file and return an associative array representation.
249
     *
250
     * This function will automatically take care of imports in each file, whether it be a child or grandchild config
251
     * file. `$configFile` should be called with 'null' when "configuration-less" mode is used.
252
     *
253
     * @param string|null $configFile     The path to the configuration file. If null, the default configuration will be
254
     *                                    used
255
     *
256
     * @return array
257
     */
258 45
    private function parseConfig($configFile = null)
259
    {
260 45
        if (null === $configFile)
261 45
        {
262 45
            return array();
263
        }
264
265 32
        $this->currentFile = $configFile;
266
267
        try
268
        {
269 32
            $this->isRecursiveImport($configFile);
270
271 32
            $parsedConfig = self::readFile($configFile);
272
273 32
            $this->handleImports($parsedConfig);
274
275 32
            unset($parsedConfig[self::IMPORT_KEYWORD]);
276 32
            return $parsedConfig;
277
        }
278 2
        catch (ParseException $e)
279
        {
280
            $this->output->error('{file}: parsing failed... {message}', array(
281
                'message' => $e->getMessage(),
282
                'file' => $configFile,
283
            ));
284
            $this->output->error('Using default configuration...');
285
        }
286 2
        catch (RecursiveConfigurationException $e)
287
        {
288 1
            $this->output->error("{file}: you can't recursively import a file that's already been imported: {import}", array(
289 1
                'file' => $configFile,
290 1
                'import' => $e->getRecursiveImport(),
291 1
            ));
292
        }
293
294 1
        return array();
295
    }
296
297
    /**
298
     * Merge the default configuration with the parsed configuration.
299
     */
300 45
    private function mergeDefaultConfiguration()
301
    {
302
        $defaultConfig = array(
303 45
            'baseurl'   => '',
304 45
            'target'    => '_site',
305
            'twig'      => array(
306 45
                'autoescape' => false,
307 45
            ),
308
            'include'   => array(
309 45
                '.htaccess',
310 45
            ),
311
            'exclude'   => array(
312 45
                'node_modules/',
313 45
                'stakx-theme.yml',
314 45
                self::DEFAULT_NAME,
315 45
            ),
316
            'templates' => array(
317 45
                'redirect' => false,
318 45
            ),
319 45
        );
320
321 45
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
322 45
    }
323
324
    /**
325
     * Warn about deprecated keywords in the configuration file.
326
     */
327 45
    private function handleDeprecations()
328
    {
329
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
330 45
        $base = $this->returnConfigOption('base');
331
332 45
        if (!is_null($base))
333 45
        {
334 2
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
335 2
        }
336 45
    }
337
338
    /**
339
     * Recursively resolve imports for a given array.
340
     *
341
     * This modifies the array in place.
342
     *
343
     * @param array $configuration
344
     */
345 32
    private function handleImports(array &$configuration)
346
    {
347 32
        if (!array_key_exists(self::IMPORT_KEYWORD, $configuration))
348 32
        {
349 32
            $this->output->debug('{file}: does not import any other files', array(
350 32
                'file' => $this->parentConfig,
351 32
            ));
352
353 32
            return;
354
        }
355
356 16
        if (!is_array($imports = $configuration[self::IMPORT_KEYWORD]))
357 16
        {
358 4
            $this->output->error('{file}: the reserved "import" keyword can only be an array');
359
360 4
            return;
361
        }
362
363 12
        $thisFile = $this->fs->getRelativePath($this->parentConfig);
364 12
        $parentConfigLocation = $this->fs->getFolderPath($thisFile);
365
366 12
        foreach ($imports as $_import)
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
367
        {
368 12
            if (!is_string($_import))
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
369 12
            {
370
                $this->output->error('{file}: invalid import: {message}', array(
371
                    'file' => $thisFile,
372
                    'message' => $_import,
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
373
                ));
374
375
                continue;
376
            }
377
378 12
            $import = $this->fs->appendPath($parentConfigLocation, $_import);
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
379
380 12
            if (!$this->isValidImport($import))
381 12
            {
382 4
                continue;
383
            }
384
385 8
            $this->output->debug('{file}: imports additional file: {import}', array(
386 8
                'file' => $thisFile,
387 8
                'import' => $import,
388 8
            ));
389
390
            try
391
            {
392 8
                $importedConfig = $this->parseConfig($import);
393 7
                $configuration = $this->mergeImports($importedConfig, $configuration);
394
            }
395 8
            catch (FileAccessDeniedException $e)
396
            {
397
                $this->output->warning('{file}: trying access file outside of project directory: {import}', array(
398
                    'file' => $thisFile,
399
                    'import' => $import,
400
                ));
401
            }
402 1
            catch (FileNotFoundException $e)
403
            {
404 1
                $this->output->warning('{file}: could not find file to import: {import}', array(
405 1
                    'file' => $thisFile,
406 1
                    'import' => $import,
407 1
                ));
408
            }
409 12
        }
410 12
    }
411
412
    /**
413
     * Check whether a given file path is a valid import.
414
     *
415
     * @param  string $filePath
416
     *
417
     * @return bool
418
     */
419 12
    private function isValidImport($filePath)
420
    {
421 12
        $thisFile = $this->fs->getRelativePath($this->parentConfig);
422 12
        $errorMsg = '';
423
424 12
        if ($this->fs->isDir($filePath))
425 12
        {
426 1
            $errorMsg = 'a directory';
427 1
        }
428 11
        elseif ($this->fs->isSymlink($filePath))
429
        {
430 1
            $errorMsg = 'a symbolically linked file';
431 1
        }
432 10
        elseif ($this->fs->absolutePath($this->currentFile) == $this->fs->absolutePath($filePath))
433
        {
434 1
            $errorMsg = 'yourself';
435 1
        }
436 9
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
437
        {
438 1
            $errorMsg = 'a non-YAML configuration';
439 1
        }
440
441 12
        if (!($noErrors = empty($errorMsg)))
442 12
        {
443 4
            $this->output->error("{file}: you can't import {message}: {import}", array(
444 4
                'file' => $thisFile,
445 4
                'message' => $errorMsg,
446
                'import' => $filePath
447 4
            ));
448 4
        }
449
450 12
        return $noErrors;
451
    }
452
453
    /**
454
     * Check whether or not a filename has already been imported in a given process.
455
     *
456
     * @param string $filePath
457
     */
458 32
    private function isRecursiveImport($filePath)
459
    {
460 32
        if (in_array($filePath, self::$configImports))
461 32
        {
462 1
            throw new RecursiveConfigurationException($filePath, sprintf(
463 1
                'The %s file has already been imported', $filePath
464 1
            ));
465
        }
466
467 32
        self::$configImports[] = $filePath;
468 32
    }
469
470
    /**
471
     * Merge the given array with existing configuration.
472
     *
473
     * @param  array $importedConfig
474
     * @param  array $existingConfig
475
     *
476
     * @return array
477
     */
478 7
    private function mergeImports(array $importedConfig, array $existingConfig)
479
    {
480 7
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $existingConfig, false);
481 7
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
482 7
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
483
484 7
        return $result;
485
    }
486
}
487