Completed
Push — master ( ab824b...23728e )
by Vladimir
02:13
created

Configuration::parse()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 12
rs 9.4285
ccs 9
cts 9
cp 1
cc 1
eloc 8
nc 1
nop 1
crap 1
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 __;
11
use allejo\stakx\Exception\FileAccessDeniedException;
12
use allejo\stakx\Exception\RecursiveConfigurationException;
13
use allejo\stakx\Filesystem\File;
14
use allejo\stakx\Utilities\ArrayUtilities;
15
use Psr\Log\LoggerAwareInterface;
16
use Psr\Log\LoggerAwareTrait;
17
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
18
use Symfony\Component\Yaml\Exception\ParseException;
19
use Symfony\Component\Yaml\Yaml;
20
21
class Configuration implements LoggerAwareInterface
22
{
23
    use LoggerAwareTrait;
24
25
    const HIGHLIGHTER_ENABLED = 'highlighter-enabled';
26
27
    const DEFAULT_NAME = '_config.yml';
28
    const IMPORT_KEYWORD = 'import';
29
    const CACHE_FOLDER = '.stakx-cache';
30
31
    private static $configImports = array();
32
33
    /**
34
     * A list of regular expressions or files directly related to stakx websites that should not be copied over to the
35
     * compiled website as an asset.
36
     *
37
     * @var array
38
     */
39
    public static $stakxSourceFiles = array('/^_(?!themes).*/', '/.twig$/');
40
41
    /**
42
     * An array representation of the main Yaml configuration.
43
     *
44
     * @var array
45
     */
46
    private $configuration;
47
48
    /**
49
     * The master configuration file for the current build.
50
     *
51
     * This is the file that will be handling imports, if any.
52
     *
53
     * @var File
54
     */
55
    private $configFile;
56
57
    /**
58
     * The current configuration file being processed.
59
     *
60
     * If there are no imports used, this value will equal $this->configFile. Otherwise, this file will equal to the
61
     * current imported configuration file that is being evaluated.
62
     *
63
     * @var File
64
     */
65
    private $currentFile;
66
67
    /**
68
     * Configuration constructor.
69
     */
70 73
    public function __construct()
71
    {
72 73
        $this->configuration = array();
73 73
    }
74
75
    ///
76
    // Getters
77
    ///
78
79
    /**
80
     * @return bool
81
     */
82 1
    public function isDebug()
83
    {
84 1
        return __::get($this->configuration, 'debug', false);
85
    }
86
87
    /**
88
     * @return string|null
89
     */
90 2
    public function getBaseUrl()
91
    {
92 2
        return __::get($this->configuration, 'baseurl');
93
    }
94
95
    public function hasDataItems()
96
    {
97
        return ($this->getDataFolders() !== null || $this->getDataSets() !== null);
98
    }
99
100
    public function hasCollections()
101
    {
102
        return ($this->getCollectionsFolders() !== null);
103
    }
104
105
    /**
106
     * @return string[]
107
     */
108 1
    public function getDataFolders()
109
    {
110 1
        return __::get($this->configuration, 'data');
111
    }
112
113
    /**
114
     * @return string[]
115
     */
116 1
    public function getDataSets()
117
    {
118 1
        return __::get($this->configuration, 'datasets');
119
    }
120
121
    /**
122
     * @return string[]
123
     */
124 1
    public function getIncludes()
125
    {
126 1
        return __::get($this->configuration, 'include', []);
127
    }
128
129
    /**
130
     * @return string[]
131
     */
132 1
    public function getExcludes()
133
    {
134 1
        return __::get($this->configuration, 'exclude', []);
135
    }
136
137
    /**
138
     * @return array
139
     */
140
    public function getHighlighterCustomLanguages()
141
    {
142
        return __::get($this->configuration, 'highlighter.languages', []);
143
    }
144
145
    /**
146
     * @return bool
147
     */
148
    public function isHighlighterEnabled()
149
    {
150
        return __::get($this->configuration, 'highlighter.enabled', true);
151
    }
152
153
    /**
154
     * @return string
155
     */
156 1
    public function getTheme()
157
    {
158 1
        return __::get($this->configuration, 'theme');
159
    }
160
161
    /**
162
     * @return array
163
     */
164 7
    public function getConfiguration()
165
    {
166 7
        return $this->configuration;
167
    }
168
169
    /**
170
     * @return string[]
171
     */
172 1
    public function getPageViewFolders()
173
    {
174 1
        return __::get($this->configuration, 'pageviews', []);
175
    }
176
177
    /**
178
     * @return string
179
     */
180 32
    public function getTargetFolder()
181
    {
182 32
        return __::get($this->configuration, 'target');
183
    }
184
185
    /**
186
     * @return string[][]
187
     */
188 1
    public function getCollectionsFolders()
189
    {
190 1
        return __::get($this->configuration, 'collections', []);
191
    }
192
193
    /**
194
     * @return bool
195
     */
196 1
    public function getTwigAutoescape()
197
    {
198 1
        return __::get($this->configuration, 'twig.autoescape');
199
    }
200
201
    /**
202
     * @return false|string
203
     */
204
    public function getRedirectTemplate()
205
    {
206
        return __::get($this->configuration, 'templates.redirect');
207
    }
208
209
    ///
210
    // Parsing
211
    ///
212
213
    /**
214
     * Parse a configuration file.
215
     *
216
     * @param File|null $configFile
217
     */
218 32
    public function parse(File $configFile = null)
219
    {
220 32
        $this->configFile = $configFile;
221 32
        self::$configImports = [];
222
223 32
        $this->configuration = $this->parseConfig($configFile);
224 32
        $this->mergeDefaultConfiguration();
225 32
        $this->handleDefaultOperations();
226 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...
227
228 32
        self::$configImports = [];
229 32
    }
230
231
    /**
232
     * Parse a given configuration file and return an associative array representation.
233
     *
234
     * This function will automatically take care of imports in each file, whether it be a child or grandchild config
235
     * file. `$configFile` should be called with 'null' when "configuration-less" mode is used.
236
     *
237
     * @param File|null $configFile The path to the configuration file. If null, the default configuration will be
238
     *                              used
239
     *
240
     * @return array
241
     */
242 32
    private function parseConfig(File $configFile = null)
243
    {
244 32
        if ($configFile === null)
245
        {
246 32
            return [];
247
        }
248
249 32
        $this->currentFile = $configFile;
250
251
        try
252
        {
253 32
            $this->isRecursiveImport($configFile);
254
255 32
            $parsedConfig = Yaml::parse($configFile->getContents());
256
257 32
            if ($parsedConfig === null)
258
            {
259
                $parsedConfig = [];
260
            }
261
262 32
            $this->handleImports($parsedConfig);
263
264 32
            unset($parsedConfig[self::IMPORT_KEYWORD]);
265
266 32
            return $parsedConfig;
267
        }
268 3
        catch (ParseException $e)
269
        {
270 1
            $this->logger->error('{file}: parsing failed... {message}', array(
271 1
                'message' => $e->getMessage(),
272 1
                'file' => $configFile,
273
            ));
274 1
            $this->logger->error('Using default configuration...');
275
        }
276 2
        catch (RecursiveConfigurationException $e)
277
        {
278 1
            $this->logger->error("{file}: you can't recursively import a file that's already been imported: {import}", array(
279 1
                'file' => $configFile,
280 1
                'import' => $e->getRecursiveImport(),
281
            ));
282
        }
283
284 2
        return [];
285
    }
286
287
    /**
288
     * Merge the default configuration with the parsed configuration.
289
     */
290 32
    private function mergeDefaultConfiguration()
291
    {
292
        $defaultConfig = array(
293 32
            'baseurl'   => '',
294 32
            'target'    => '_site',
295
            'twig'      => array(
296
                'autoescape' => false,
297
            ),
298
            'include'   => array(
299
                '.htaccess',
300
            ),
301
            'exclude'   => array(
302 32
                'node_modules/',
303 32
                'stakx-theme.yml',
304 32
                '/tmp___$/',
305 32
                self::DEFAULT_NAME,
306
            ),
307
            'templates' => array(
308
                'redirect' => false,
309
            ),
310
            'highlighter' => array(
311
                'enabled' => true,
312
                'languages' => array(),
313
            ),
314
            'build' => array(
315
                'preserveCase' => false
316
            ),
317
        );
318
319 32
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
320 32
    }
321
322
    /**
323
     * Warn about deprecated keywords in the configuration file.
324
     */
325 32
    private function handleDeprecations()
326
    {
327
        // Nothing deprecated right now
328 32
    }
329
330
    /**
331
     * Recursively resolve imports for a given array.
332
     *
333
     * This modifies the array in place.
334
     *
335
     * @param array $configuration
336
     */
337 32
    private function handleImports(array &$configuration)
338
    {
339 32
        if (!isset($configuration[self::IMPORT_KEYWORD]))
340
        {
341 32
            $this->logger->debug('{file}: does not import any other files', array(
342 32
                'file' => $this->currentFile->getRelativeFilePath(),
343
            ));
344
345 32
            return;
346
        }
347
348 17
        if (!is_array($imports = $configuration[self::IMPORT_KEYWORD]))
349
        {
350 3
            $this->logger->error('{file}: the reserved "import" keyword can only be an array');
351
352 3
            return;
353
        }
354
355 14
        foreach ($imports as $import)
356
        {
357 14
            $this->handleImport($import, $configuration);
358
        }
359 14
    }
360
361
    /**
362
     * Resolve a single import definition.
363
     *
364
     * @param string $importDef     The path for a given import; this will be treated as a relative path to the parent
365
     *                              configuration
366
     * @param array  $configuration The array representation of the current configuration; this will be modified in place
367
     */
368 14
    private function handleImport($importDef, array &$configuration)
369
    {
370 14
        if (!is_string($importDef))
371
        {
372 3
            $this->logger->error('{file}: invalid import: {message}', array(
373 3
                'file' => $this->configFile->getRelativeFilePath(),
374 3
                'message' => $importDef,
375
            ));
376
377 3
            return;
378
        }
379
380 11
        $import = $this->configFile->createFileForRelativePath($importDef);
381
382 11
        if (!$this->isValidImport($import))
383
        {
384 3
            return;
385
        }
386
387 8
        $this->logger->debug('{file}: imports additional file: {import}', array(
388 8
            'file' => $this->configFile->getRelativeFilePath(),
389 8
            'import' => $import->getRelativeFilePath(),
390
        ));
391
392
        try
393
        {
394 8
            $importedConfig = $this->parseConfig($import);
395 7
            $configuration = $this->mergeImports($importedConfig, $configuration);
396
        }
397 1
        catch (FileAccessDeniedException $e)
398
        {
399
            $this->logger->warning('{file}: trying access file outside of project directory: {import}', array(
400
                'file' => $this->configFile->getRelativeFilePath(),
401
                'import' => $import,
402
            ));
403
        }
404 1
        catch (FileNotFoundException $e)
405
        {
406 1
            $this->logger->warning('{file}: could not find file to import: {import}', array(
407 1
                'file' => $this->configFile->getRelativeFilePath(),
408 1
                'import' => $import,
409
            ));
410
        }
411 8
    }
412
413
    /**
414
     * Check whether a given file path is a valid import.
415
     *
416
     * @param  File $filePath
417
     *
418
     * @return bool
419
     */
420 11
    private function isValidImport(File $filePath)
421
    {
422 11
        $errorMsg = '';
423
424 11
        if ($filePath->isDir())
425
        {
426 1
            $errorMsg = 'a directory';
427
        }
428 10
        elseif ($filePath->isLink())
429
        {
430
            $errorMsg = 'a symbolically linked file';
431
        }
432 10
        elseif ($this->currentFile->getAbsolutePath() == $filePath->getAbsolutePath())
433
        {
434 1
            $errorMsg = 'yourself';
435
        }
436 9
        elseif (($ext = $filePath->getExtension()) != 'yml' && $ext != 'yaml')
437
        {
438 1
            $errorMsg = 'a non-YAML configuration';
439
        }
440
441 11
        if (!($noErrors = empty($errorMsg)))
442
        {
443 3
            $this->logger->error("{file}: you can't import {message}: {import}", array(
444 3
                'file' => $this->configFile->getRelativeFilePath(),
445 3
                'message' => $errorMsg,
446 3
                'import' => $filePath
447
            ));
448
        }
449
450 11
        return $noErrors;
451
    }
452
453
    /**
454
     * Check whether or not a filename has already been imported in a given process.
455
     *
456
     * @param File $filePath
457
     */
458 32
    private function isRecursiveImport(File $filePath)
459
    {
460 32
        if (in_array($filePath->getRelativeFilePath(), self::$configImports))
461
        {
462 1
            throw new RecursiveConfigurationException($filePath, sprintf(
463 1
                'The %s file has already been imported', $filePath->getRelativeFilePath()
464
            ));
465
        }
466
467 32
        self::$configImports[] = $filePath->getRelativeFilePath();
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 32
    private function handleDefaultOperations()
488
    {
489 32
        if (substr($this->getTargetFolder(), 0, 1) != '_')
490
        {
491
            $this->configuration['exclude'][] = $this->getTargetFolder();
492
        }
493
494 32
        Service::setParameter('build.preserveCase', $this->configuration['build']['preserveCase']);
495 32
    }
496
}
497