Completed
Push — master ( c2b5cc...89bfcf )
by Vladimir
03:33
created

Configuration::isHighlighterEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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