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