Completed
Pull Request — master (#69)
by Vladimir
02:48
created

Configuration::getTargetFolder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

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