Completed
Push — master ( 89bfcf...4ab803 )
by Vladimir
02:41
created

Configuration::getHighlighterCustomLanguages()   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 50
    public function getTargetFolder()
186
    {
187 50
        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->handleDefaultOperations();
262 50
        $this->handleDeprecations();
263
264 50
        self::$configImports = array();
265 50
    }
266
267
    /**
268
     * Parse a given configuration file and return an associative array representation.
269
     *
270
     * This function will automatically take care of imports in each file, whether it be a child or grandchild config
271
     * file. `$configFile` should be called with 'null' when "configuration-less" mode is used.
272
     *
273
     * @param string|null $configFile     The path to the configuration file. If null, the default configuration will be
274
     *                                    used
275
     *
276
     * @return array
277
     */
278 50
    private function parseConfig($configFile = null)
279
    {
280 50
        if (null === $configFile)
281
        {
282 50
            return array();
283
        }
284
285 36
        $this->currentFile = $configFile;
286
287
        try
288
        {
289 36
            $this->isRecursiveImport($configFile);
290
291 36
            $parsedConfig = self::readFile($configFile);
292
293 36
            $this->handleImports($parsedConfig);
294
295 36
            unset($parsedConfig[self::IMPORT_KEYWORD]);
296 36
            return $parsedConfig;
297
        }
298 3
        catch (ParseException $e)
299
        {
300 1
            $this->output->error('{file}: parsing failed... {message}', array(
301 1
                'message' => $e->getMessage(),
302 1
                'file' => $configFile,
303
            ));
304 1
            $this->output->error('Using default configuration...');
305
        }
306 2
        catch (RecursiveConfigurationException $e)
307
        {
308 1
            $this->output->error("{file}: you can't recursively import a file that's already been imported: {import}", array(
309 1
                'file' => $configFile,
310 1
                'import' => $e->getRecursiveImport(),
311
            ));
312
        }
313
314 2
        return array();
315
    }
316
317
    /**
318
     * Merge the default configuration with the parsed configuration.
319
     */
320 50
    private function mergeDefaultConfiguration()
321
    {
322
        $defaultConfig = array(
323 50
            'baseurl'   => '',
324 50
            'target'    => '_site',
325
            'twig'      => array(
326
                'autoescape' => false,
327
            ),
328
            'include'   => array(
329
                '.htaccess',
330
            ),
331
            'exclude'   => array(
332 50
                'node_modules/',
333 50
                'stakx-theme.yml',
334 50
                '/tmp___$/',
335 50
                self::DEFAULT_NAME,
336
            ),
337
            'templates' => array(
338
                'redirect' => false,
339
            ),
340
            'highlighter' => array(
341
                'enabled' => true,
342
                'languages' => array(),
343
            ),
344
            'build' => array(
345
                'preserveCase' => false
346
            ),
347
        );
348
349 50
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
350 50
    }
351
352
    /**
353
     * Warn about deprecated keywords in the configuration file.
354
     */
355 50
    private function handleDeprecations()
356
    {
357
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
358 50
        $base = $this->returnConfigOption('base');
359
360 50
        if (!is_null($base))
361
        {
362 2
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
363
        }
364 50
    }
365
366
    /**
367
     * Recursively resolve imports for a given array.
368
     *
369
     * This modifies the array in place.
370
     *
371
     * @param array $configuration
372
     */
373 36
    private function handleImports(array &$configuration)
374
    {
375 36
        if (!array_key_exists(self::IMPORT_KEYWORD, $configuration))
376
        {
377 36
            $this->output->debug('{file}: does not import any other files', array(
378 36
                'file' => $this->parentConfig,
379
            ));
380
381 36
            return;
382
        }
383
384 19
        if (!is_array($imports = $configuration[self::IMPORT_KEYWORD]))
385
        {
386 4
            $this->output->error('{file}: the reserved "import" keyword can only be an array');
387
388 4
            return;
389
        }
390
391 15
        $parentConfigLocation = $this->fs->getFolderPath($this->parentConfig);
392
393 15
        foreach ($imports as $import)
394
        {
395 15
            $this->handleImport($import, $parentConfigLocation, $configuration);
396
        }
397 15
    }
398
399
    /**
400
     * Resolve a single import definition.
401
     *
402
     * @param string $importDef     The path for a given import; this will be treated as a relative path to the parent
403
     *                              configuration
404
     * @param string $parentConfLoc The path to the parent configuration
405
     * @param array  $configuration The array representation of the current configuration; this will be modified in place
406
     */
407 15
    private function handleImport($importDef, $parentConfLoc, array &$configuration)
408
    {
409 15
        if (!is_string($importDef))
410
        {
411 3
            $this->output->error('{file}: invalid import: {message}', array(
412 3
                'file' => $this->parentConfig,
413 3
                'message' => $importDef,
414
            ));
415
416 3
            return;
417
        }
418
419 12
        $import = $this->fs->appendPath($parentConfLoc, $importDef);
420
421 12
        if (!$this->isValidImport($import))
422
        {
423 4
            return;
424
        }
425
426 8
        $this->output->debug('{file}: imports additional file: {import}', array(
427 8
            'file' => $this->parentConfig,
428 8
            'import' => $import,
429
        ));
430
431
        try
432
        {
433 8
            $importedConfig = $this->parseConfig($import);
434 7
            $configuration = $this->mergeImports($importedConfig, $configuration);
435
        }
436 1
        catch (FileAccessDeniedException $e)
437
        {
438
            $this->output->warning('{file}: trying access file outside of project directory: {import}', array(
439
                'file' => $this->parentConfig,
440
                'import' => $import,
441
            ));
442
        }
443 1
        catch (FileNotFoundException $e)
444
        {
445 1
            $this->output->warning('{file}: could not find file to import: {import}', array(
446 1
                'file' => $this->parentConfig,
447 1
                'import' => $import,
448
            ));
449
        }
450 8
    }
451
452
    /**
453
     * Check whether a given file path is a valid import.
454
     *
455
     * @param  string $filePath
456
     *
457
     * @return bool
458
     */
459 12
    private function isValidImport($filePath)
460
    {
461 12
        $errorMsg = '';
462
463 12
        if ($this->fs->isDir($filePath))
464
        {
465 1
            $errorMsg = 'a directory';
466
        }
467 11
        elseif ($this->fs->isSymlink($filePath))
468
        {
469 1
            $errorMsg = 'a symbolically linked file';
470
        }
471 10
        elseif ($this->fs->absolutePath($this->currentFile) == $this->fs->absolutePath($filePath))
472
        {
473 1
            $errorMsg = 'yourself';
474
        }
475 9
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
476
        {
477 1
            $errorMsg = 'a non-YAML configuration';
478
        }
479
480 12
        if (!($noErrors = empty($errorMsg)))
481
        {
482 4
            $this->output->error("{file}: you can't import {message}: {import}", array(
483 4
                'file' => $this->parentConfig,
484 4
                'message' => $errorMsg,
485 4
                'import' => $filePath
486
            ));
487
        }
488
489 12
        return $noErrors;
490
    }
491
492
    /**
493
     * Check whether or not a filename has already been imported in a given process.
494
     *
495
     * @param string $filePath
496
     */
497 36
    private function isRecursiveImport($filePath)
498
    {
499 36
        if (in_array($filePath, self::$configImports))
500
        {
501 1
            throw new RecursiveConfigurationException($filePath, sprintf(
502 1
                'The %s file has already been imported', $filePath
503
            ));
504
        }
505
506 36
        self::$configImports[] = $filePath;
507 36
    }
508
509
    /**
510
     * Merge the given array with existing configuration.
511
     *
512
     * @param  array $importedConfig
513
     * @param  array $existingConfig
514
     *
515
     * @return array
516
     */
517 7
    private function mergeImports(array $importedConfig, array $existingConfig)
518
    {
519 7
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $existingConfig, false);
520 7
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
521 7
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
522
523 7
        return $result;
524
    }
525
526 50
    private function handleDefaultOperations()
527
    {
528 50
        if (substr($this->getTargetFolder(), 0, 1) != '_')
529
        {
530
            $this->configuration['exclude'][] = $this->getTargetFolder();
531
        }
532 50
    }
533
}
534