Completed
Push — master ( 808f8c...952fde )
by Vladimir
11s
created

Configuration::parseConfig()   B

Complexity

Conditions 5
Paths 11

Size

Total Lines 44
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 5.0026

Importance

Changes 0
Metric Value
dl 0
loc 44
ccs 20
cts 21
cp 0.9524
rs 8.439
c 0
b 0
f 0
cc 5
eloc 22
nc 11
nop 1
crap 5.0026
1
<?php
2
3
/**
4
 * @copyright 2018 Vladimir Jimenez
5
 * @license   https://github.com/stakx-io/stakx/blob/master/LICENSE.md MIT
6
 */
7
8
namespace allejo\stakx;
9
10
use __;
11
use allejo\stakx\Exception\RecursiveConfigurationException;
12
use allejo\stakx\Filesystem\File;
13
use allejo\stakx\Utilities\ArrayUtilities;
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerAwareTrait;
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
    use LoggerAwareTrait;
23
24
    const HIGHLIGHTER_ENABLED = 'highlighter-enabled';
25
26
    const DEFAULT_NAME = '_config.yml';
27
    const IMPORT_KEYWORD = 'import';
28
    const CACHE_FOLDER = '.stakx-cache';
29
30
    private static $configImports = [];
31
32
    /**
33
     * A list of regular expressions or files directly related to stakx websites that should not be copied over to the
34
     * compiled website as an asset.
35
     *
36
     * @var array
37
     */
38
    public static $stakxSourceFiles = ['/^_(?!themes).*/', '/.twig$/'];
39
40
    /**
41
     * An array representation of the main Yaml configuration.
42
     *
43
     * @var array
44
     */
45
    private $configuration;
46
47
    /**
48
     * The master configuration file for the current build.
49
     *
50
     * This is the file that will be handling imports, if any.
51
     *
52
     * @var File
53
     */
54
    private $configFile;
55
56
    /**
57
     * The current configuration file being processed.
58
     *
59
     * If there are no imports used, this value will equal $this->configFile. Otherwise, this file will equal to the
60
     * current imported configuration file that is being evaluated.
61
     *
62
     * @var File
63
     */
64
    private $currentFile;
65
66
    /**
67
     * Configuration constructor.
68
     */
69 73
    public function __construct()
70
    {
71 73
        $this->configuration = [];
72 73
    }
73
74
    ///
75
    // Getters
76
    ///
77
78
    /**
79
     * @return bool
80
     */
81 1
    public function isDebug()
82
    {
83 1
        return __::get($this->configuration, 'debug', false);
84
    }
85
86
    /**
87
     * @return string|null
88
     */
89 2
    public function getBaseUrl()
90
    {
91 2
        return __::get($this->configuration, 'baseurl');
92
    }
93
94
    public function hasDataItems()
95
    {
96
        return $this->getDataFolders() !== null || $this->getDataSets() !== null;
97
    }
98
99
    public function hasCollections()
100
    {
101
        return $this->getCollectionsFolders() !== null;
102
    }
103
104
    /**
105
     * @return string[]
106
     */
107 1
    public function getDataFolders()
108
    {
109 1
        return __::get($this->configuration, 'data');
110
    }
111
112
    /**
113
     * @return string[]
114
     */
115 1
    public function getDataSets()
116
    {
117 1
        return __::get($this->configuration, 'datasets');
118
    }
119
120
    /**
121
     * @return string[]
122
     */
123 1
    public function getIncludes()
124
    {
125 1
        return __::get($this->configuration, 'include', []);
126
    }
127
128
    /**
129
     * @return string[]
130
     */
131 1
    public function getExcludes()
132
    {
133 1
        return __::get($this->configuration, 'exclude', []);
134
    }
135
136
    /**
137
     * @return array
138
     */
139
    public function getHighlighterCustomLanguages()
140
    {
141
        return __::get($this->configuration, 'highlighter.languages', []);
142
    }
143
144
    /**
145
     * @return bool
146
     */
147
    public function isHighlighterEnabled()
148
    {
149
        return __::get($this->configuration, 'highlighter.enabled', true);
150
    }
151
152
    /**
153
     * @return string
154
     */
155 1
    public function getTheme()
156
    {
157 1
        return __::get($this->configuration, 'theme');
158
    }
159
160
    /**
161
     * @return array
162
     */
163 7
    public function getConfiguration()
164
    {
165 7
        return $this->configuration;
166
    }
167
168
    /**
169
     * @return string[]
170
     */
171 1
    public function getPageViewFolders()
172
    {
173 1
        return __::get($this->configuration, 'pageviews', []);
174
    }
175
176
    /**
177
     * @return string
178
     */
179 32
    public function getTargetFolder()
180
    {
181 32
        return __::get($this->configuration, 'target');
182
    }
183
184
    /**
185
     * @return string[][]
186
     */
187 1
    public function getCollectionsFolders()
188
    {
189 1
        return __::get($this->configuration, 'collections', []);
190
    }
191
192
    /**
193
     * @return bool
194
     */
195 1
    public function getTwigAutoescape()
196
    {
197 1
        return __::get($this->configuration, 'twig.autoescape');
198
    }
199
200
    /**
201
     * @return false|string
202
     */
203
    public function getRedirectTemplate()
204
    {
205
        return __::get($this->configuration, 'templates.redirect');
206
    }
207
208
    ///
209
    // Parsing
210
    ///
211
212
    /**
213
     * Parse a configuration file.
214
     *
215
     * @param File|null $configFile
216
     */
217 32
    public function parse(File $configFile = null)
218
    {
219 32
        $this->configFile = $configFile;
220 32
        self::$configImports = [];
221
222 32
        $this->configuration = $this->parseConfig($configFile);
223 32
        $this->mergeDefaultConfiguration();
224 32
        $this->handleDefaultOperations();
225 32
        $this->handleDeprecations();
0 ignored issues
show
Unused Code introduced by
The call to the method allejo\stakx\Configuration::handleDeprecations() seems un-needed as the method has no side-effects.

PHP Analyzer performs a side-effects analysis of your code. A side-effect is basically anything that might be visible after the scope of the method is left.

Let’s take a look at an example:

class User
{
    private $email;

    public function getEmail()
    {
        return $this->email;
    }

    public function setEmail($email)
    {
        $this->email = $email;
    }
}

If we look at the getEmail() method, we can see that it has no side-effect. Whether you call this method or not, no future calls to other methods are affected by this. As such code as the following is useless:

$user = new User();
$user->getEmail(); // This line could safely be removed as it has no effect.

On the hand, if we look at the setEmail(), this method _has_ side-effects. In the following case, we could not remove the method call:

$user = new User();
$user->setEmail('email@domain'); // This line has a side-effect (it changes an
                                 // instance variable).
Loading history...
226
227 32
        self::$configImports = [];
228 32
    }
229
230
    /**
231
     * Parse a given configuration file and return an associative array representation.
232
     *
233
     * This function will automatically take care of imports in each file, whether it be a child or grandchild config
234
     * file. `$configFile` should be called with 'null' when "configuration-less" mode is used.
235
     *
236
     * @param File|null $configFile The path to the configuration file. If null, the default configuration will be
237
     *                              used
238
     *
239
     * @return array
240
     */
241 32
    private function parseConfig(File $configFile = null)
242
    {
243 32
        if ($configFile === null)
244
        {
245 32
            return [];
246
        }
247
248 32
        $this->currentFile = $configFile;
249
250
        try
251
        {
252 32
            $this->isRecursiveImport($configFile);
253
254 32
            $parsedConfig = Yaml::parse($configFile->getContents());
255
256 32
            if ($parsedConfig === null)
257
            {
258
                $parsedConfig = [];
259
            }
260
261 32
            $this->handleImports($parsedConfig);
262
263 32
            unset($parsedConfig[self::IMPORT_KEYWORD]);
264
265 32
            return $parsedConfig;
266
        }
267 3
        catch (ParseException $e)
268
        {
269 1
            $this->logger->error('{file}: parsing failed... {message}', [
270 1
                'message' => $e->getMessage(),
271 1
                'file' => $configFile,
272
            ]);
273 1
            $this->logger->error('Using default configuration...');
274
        }
275 2
        catch (RecursiveConfigurationException $e)
276
        {
277 1
            $this->logger->error("{file}: you can't recursively import a file that's already been imported: {import}", [
278 1
                'file' => $configFile,
279 1
                'import' => $e->getRecursiveImport(),
280
            ]);
281
        }
282
283 2
        return [];
284
    }
285
286
    /**
287
     * Merge the default configuration with the parsed configuration.
288
     */
289 32
    private function mergeDefaultConfiguration()
290
    {
291
        $defaultConfig = [
292 32
            'baseurl' => '',
293 32
            'target' => '_site',
294
            'twig' => [
295
                'autoescape' => false,
296
            ],
297
            'include' => [
298
                '.htaccess',
299
            ],
300
            'exclude' => [
301 32
                'node_modules/',
302 32
                'stakx-theme.yml',
303 32
                '/tmp___$/',
304 32
                self::DEFAULT_NAME,
305
            ],
306
            'templates' => [
307
                'redirect' => false,
308
            ],
309
            'highlighter' => [
310
                'enabled' => true,
311
                'languages' => [],
312
            ],
313
            'build' => [
314
                'preserveCase' => false,
315
            ],
316
        ];
317
318 32
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
319 32
    }
320
321
    /**
322
     * Warn about deprecated keywords in the configuration file.
323
     */
324 32
    private function handleDeprecations()
325
    {
326
        // Nothing deprecated right now
327 32
    }
328
329
    /**
330
     * Recursively resolve imports for a given array.
331
     *
332
     * This modifies the array in place.
333
     *
334
     * @param array $configuration
335
     */
336 32
    private function handleImports(array &$configuration)
337
    {
338 32
        if (!isset($configuration[self::IMPORT_KEYWORD]))
339
        {
340 32
            $this->logger->debug('{file}: does not import any other files', [
341 32
                'file' => $this->currentFile->getRelativeFilePath(),
342
            ]);
343
344 32
            return;
345
        }
346
347 17
        if (!is_array($imports = $configuration[self::IMPORT_KEYWORD]))
348
        {
349 3
            $this->logger->error('{file}: the reserved "import" keyword can only be an array');
350
351 3
            return;
352
        }
353
354 14
        foreach ($imports as $import)
355
        {
356 14
            $this->handleImport($import, $configuration);
357
        }
358 14
    }
359
360
    /**
361
     * Resolve a single import definition.
362
     *
363
     * @param string $importDef     The path for a given import; this will be treated as a relative path to the parent
364
     *                              configuration
365
     * @param array  $configuration The array representation of the current configuration; this will be modified in place
366
     */
367 14
    private function handleImport($importDef, array &$configuration)
368
    {
369 14
        if (!is_string($importDef))
370
        {
371 3
            $this->logger->error('{file}: invalid import: {message}', [
372 3
                'file' => $this->configFile->getRelativeFilePath(),
373 3
                'message' => $importDef,
374
            ]);
375
376 3
            return;
377
        }
378
379 11
        $import = $this->configFile->createFileForRelativePath($importDef);
380
381 11
        if (!$this->isValidImport($import))
382
        {
383 3
            return;
384
        }
385
386 8
        $this->logger->debug('{file}: imports additional file: {import}', [
387 8
            'file' => $this->configFile->getRelativeFilePath(),
388 8
            'import' => $import->getRelativeFilePath(),
389
        ]);
390
391
        try
392
        {
393 8
            $importedConfig = $this->parseConfig($import);
394 7
            $configuration = $this->mergeImports($importedConfig, $configuration);
395
        }
396 1
        catch (FileNotFoundException $e)
397
        {
398 1
            $this->logger->warning('{file}: could not find file to import: {import}', [
399 1
                'file' => $this->configFile->getRelativeFilePath(),
400 1
                'import' => $import,
401
            ]);
402
        }
403 8
    }
404
405
    /**
406
     * Check whether a given file path is a valid import.
407
     *
408
     * @param File $filePath
409
     *
410
     * @return bool
411
     */
412 11
    private function isValidImport(File $filePath)
413
    {
414 11
        $errorMsg = '';
415
416 11
        if ($filePath->isDir())
417
        {
418 1
            $errorMsg = 'a directory';
419
        }
420 10
        elseif ($filePath->isLink())
421
        {
422
            $errorMsg = 'a symbolically linked file';
423
        }
424 10
        elseif ($this->currentFile->getAbsolutePath() == $filePath->getAbsolutePath())
425
        {
426 1
            $errorMsg = 'yourself';
427
        }
428 9
        elseif (($ext = $filePath->getExtension()) != 'yml' && $ext != 'yaml')
429
        {
430 1
            $errorMsg = 'a non-YAML configuration';
431
        }
432
433 11
        if (!($noErrors = empty($errorMsg)))
434
        {
435 3
            $this->logger->error("{file}: you can't import {message}: {import}", [
436 3
                'file' => $this->configFile->getRelativeFilePath(),
437 3
                'message' => $errorMsg,
438 3
                'import' => $filePath,
439
            ]);
440
        }
441
442 11
        return $noErrors;
443
    }
444
445
    /**
446
     * Check whether or not a filename has already been imported in a given process.
447
     *
448
     * @param File $filePath
449
     */
450 32
    private function isRecursiveImport(File $filePath)
451
    {
452 32
        if (in_array($filePath->getRelativeFilePath(), self::$configImports))
453
        {
454 1
            throw new RecursiveConfigurationException($filePath, sprintf(
455 1
                'The %s file has already been imported', $filePath->getRelativeFilePath()
456
            ));
457
        }
458
459 32
        self::$configImports[] = $filePath->getRelativeFilePath();
460 32
    }
461
462
    /**
463
     * Merge the given array with existing configuration.
464
     *
465
     * @param array $importedConfig
466
     * @param array $existingConfig
467
     *
468
     * @return array
469
     */
470 7
    private function mergeImports(array $importedConfig, array $existingConfig)
471
    {
472 7
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $existingConfig, false);
473 7
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
474 7
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
475
476 7
        return $result;
477
    }
478
479 32
    private function handleDefaultOperations()
480
    {
481 32
        if (substr($this->getTargetFolder(), 0, 1) != '_')
482
        {
483
            $this->configuration['exclude'][] = $this->getTargetFolder();
484
        }
485
486 32
        Service::setParameter('build.preserveCase', $this->configuration['build']['preserveCase']);
487 32
    }
488
}
489