Completed
Pull Request — master (#75)
by Vladimir
02:16
created

Configuration::getTargetFolder()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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