Completed
Push — master ( 7b9015...c5b311 )
by Vladimir
26s queued 11s
created

Configuration::isHighlighterUsingLineNumbers()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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