Completed
Pull Request — master (#42)
by Vladimir
03:07
created

Configuration::defaultConfiguration()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 23
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 15
nc 1
nop 0
dl 0
loc 23
ccs 8
cts 8
cp 1
crap 1
rs 9.0856
c 0
b 0
f 0

4 Methods

Rating   Name   Duplication   Size   Complexity  
A Configuration::getExcludes() 0 4 1
A Configuration::getTheme() 0 4 1
A Configuration::getConfiguration() 0 4 1
A Configuration::getPageViewFolders() 0 4 1
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 DEFAULT_NAME = '_config.yml';
23
    const IMPORT_KEYWORD = 'import';
24
25
    private static $configImports = array();
26
27
    /**
28
     * A list of regular expressions or files directly related to stakx websites that should not be copied over to the
29
     * compiled website as an asset.
30
     *
31
     * @var array
32
     */
33
    public static $stakxSourceFiles = array('/^_(?!themes).*/', '/.twig$/');
34
35
    /**
36
     * An array representation of the main Yaml configuration.
37
     *
38
     * @var array
39
     */
40
    private $configuration;
41
42
    /**
43
     * @var string
44
     */
45
    private $parentConfig;
46
47
    /** @var string */
48
    private $currentFile;
49
50
    /**
51
     * @var LoggerInterface
52
     */
53
    private $output;
54
55
    /**
56
     * @var Filesystem
57
     */
58
    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...
59
60
    /**
61
     * Configuration constructor.
62
     */
63 45
    public function __construct()
64
    {
65 45
        $this->configuration = array();
66 45
        $this->fs = new Filesystem();
67 45
    }
68
69 32
    private static function readFile($filePath)
70
    {
71 32
        $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...
72 32
        $fileRaw = $fs->safeReadFile($filePath);
73 32
        $parsed = Yaml::parse($fileRaw);
74
75 32
        return (null === $parsed) ? array() : $parsed;
76
    }
77
78 45
    public function parse($configFile = null)
79
    {
80 45
        $this->parentConfig = $configFile;
81 45
        self::$configImports = array();
82
83 45
        $this->configuration = $this->parseConfig($configFile);
84 45
        $this->mergeDefaultConfiguration();
85 45
        $this->handleDeprecations();
86
87 45
        self::$configImports = array();
88 45
    }
89
90
    /**
91
     * Parse a given configuration file and configure this Configuration instance.
92
     *
93
     * This function should be called with 'null' passed when "configuration-less" mode is used
94
     *
95
     * @param string|null $configFile     The path to the configuration file. If null, the default configuration will be
96
     *                                    used
97
     *
98
     * @return array
99
     */
100 45
    private function parseConfig($configFile = null)
101
    {
102 45
        if (null === $configFile)
103 45
        {
104 45
            return array();
105
        }
106
107 32
        $this->currentFile = $configFile;
108
109
        try
110
        {
111 32
            $this->isRecursiveImport($configFile);
112
113 32
            $parsedConfig = self::readFile($configFile);
114
115 32
            $this->handleImports($parsedConfig);
116
117 32
            unset($parsedConfig[self::IMPORT_KEYWORD]);
118 32
            return $parsedConfig;
119
        }
120 2
        catch (ParseException $e)
121
        {
122
            $this->output->error('{file}: parsing failed... {message}', array(
123
                'message' => $e->getMessage(),
124
                'file' => $configFile,
125
            ));
126
            $this->output->error('Using default configuration...');
127
        }
128 2
        catch (RecursiveConfigurationException $e)
129
        {
130 1
            $this->output->error("{file}: you can't recursively import a file that's already been imported: {import}", array(
131 1
                'file' => $configFile,
132 1
                'import' => $e->getRecursiveImport(),
133 1
            ));
134
        }
135
136 1
        return array();
137
    }
138
139
    /**
140
     * {@inheritdoc}
141
     */
142 45
    public function setLogger(LoggerInterface $logger)
143
    {
144 45
        $this->output = $logger;
145 45
    }
146
147 14
    public function isDebug()
148
    {
149 14
        return $this->returnConfigOption('debug', false);
150
    }
151
152
    /**
153
     * @TODO 1.0.0 Remove support for 'base' in next major release; it has been replaced by 'baseurl'
154
     *
155
     * @return mixed|null
156
     */
157 4
    public function getBaseUrl()
158
    {
159 4
        $base = $this->returnConfigOption('base');
160 4
        $baseUrl = $this->returnConfigOption('baseurl');
161
162 4
        if (is_null($base) || (!empty($baseUrl)))
163 4
        {
164 3
            return $baseUrl;
165
        }
166
167 1
        return $base;
168
    }
169
170
    /**
171
     * @return string[]
172
     */
173 1
    public function getDataFolders()
174
    {
175 1
        return $this->returnConfigOption('data');
176
    }
177
178
    /**
179
     * @return string[]
180
     */
181 1
    public function getDataSets()
182
    {
183 1
        return $this->returnConfigOption('datasets');
184
    }
185
186 1
    public function getIncludes()
187
    {
188 1
        return $this->returnConfigOption('include', array());
189
    }
190
191 1
    public function getExcludes()
192
    {
193 1
        return $this->returnConfigOption('exclude', array());
194
    }
195
196 14
    public function getTheme()
197
    {
198 14
        return $this->returnConfigOption('theme');
199
    }
200
201 7
    public function getConfiguration()
202
    {
203 7
        return $this->configuration;
204
    }
205
206 1
    public function getPageViewFolders()
207 1
    {
208 1
        return $this->returnConfigOption('pageviews');
209
    }
210
211 2
    public function getTargetFolder()
212
    {
213 2
        return $this->returnConfigOption('target');
214
    }
215
216 1
    public function getCollectionsFolders()
217
    {
218 1
        return $this->returnConfigOption('collections');
219
    }
220
221 14
    public function getTwigAutoescape()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
222
    {
223 14
        return $this->configuration['twig']['autoescape'];
224
    }
225
226
    public function getRedirectTemplate()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
227
    {
228
        return $this->configuration['templates']['redirect'];
229
    }
230
231
    /**
232
     * Return the specified configuration option if available, otherwise return the default.
233
     *
234
     * @param string     $name    The configuration option to lookup
235
     * @param mixed|null $default The default value returned if the configuration option isn't found
236
     *
237
     * @return mixed|null
238
     */
239 45
    private function returnConfigOption($name, $default = null)
240
    {
241 45
        return isset($this->configuration[$name]) ? $this->configuration[$name] : $default;
242
    }
243
244 45
    private function mergeDefaultConfiguration()
245
    {
246
        $defaultConfig = array(
247 45
            'baseurl'   => '',
248 45
            'target'    => '_site',
249
            'twig'      => array(
250 45
                'autoescape' => false,
251 45
            ),
252
            'include'   => array(
253 45
                '.htaccess',
254 45
            ),
255
            'exclude'   => array(
256 45
                'node_modules/',
257 45
                'stakx-theme.yml',
258 45
                self::DEFAULT_NAME,
259 45
            ),
260
            'templates' => array(
261 45
                'redirect' => false,
262 45
            ),
263 45
        );
264
265 45
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
266 45
    }
267
268 45
    private function handleDeprecations()
269
    {
270
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
271 45
        $base = $this->returnConfigOption('base');
272
273 45
        if (!is_null($base))
274 45
        {
275 2
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
276 2
        }
277 45
    }
278
279 32
    private function handleImports(&$configuration)
280
    {
281 32
        if (!array_key_exists(self::IMPORT_KEYWORD, $configuration))
282 32
        {
283 32
            $this->output->debug('{file}: does not import any other files', array(
284 32
                'file' => $this->parentConfig,
285 32
            ));
286
287 32
            return;
288
        }
289
290 16
        if (!is_array($imports = $configuration[self::IMPORT_KEYWORD]))
291 16
        {
292 4
            $this->output->error('{file}: the reserved "import" keyword can only be an array');
293
294 4
            return;
295
        }
296
297 12
        $thisFile = $this->fs->getRelativePath($this->parentConfig);
298 12
        $parentConfigLocation = $this->fs->getFolderPath($thisFile);
299
300 12
        foreach ($imports as $_import)
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
301
        {
302 12
            if (!is_string($_import))
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
303 12
            {
304
                $this->output->error('{file}: invalid import: {message}', array(
305
                    'file' => $thisFile,
306
                    'message' => $_import,
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
307
                ));
308
309
                continue;
310
            }
311
312 12
            $import = $this->fs->appendPath($parentConfigLocation, $_import);
0 ignored issues
show
Coding Style introduced by
$_import does not seem to conform to the naming convention (^[a-z][a-zA-Z0-9]*$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
313
314 12
            if (!$this->isValidImport($import))
315 12
            {
316 4
                continue;
317
            }
318
319 8
            $this->output->debug('{file}: imports additional file: {import}', array(
320 8
                'file' => $thisFile,
321 8
                'import' => $import,
322 8
            ));
323
324
            try
325
            {
326 8
                $importedConfig = $this->parseConfig($import);
327 7
                $configuration = $this->mergeImports($importedConfig, $configuration);
328
            }
329 8
            catch (FileAccessDeniedException $e)
330
            {
331
                $this->output->warning('{file}: trying access file outside of project directory: {import}', array(
332
                    'file' => $thisFile,
333
                    'import' => $import,
334
                ));
335
            }
336 1
            catch (FileNotFoundException $e)
337
            {
338 1
                $this->output->warning('{file}: could not find file to import: {import}', array(
339 1
                    'file' => $thisFile,
340 1
                    'import' => $import,
341 1
                ));
342
            }
343 12
        }
344 12
    }
345
346
    /**
347
     * Check whether a given file path is a valid import
348
     *
349
     * @param  string $filePath
350
     *
351
     * @return bool
352
     */
353 12
    private function isValidImport($filePath)
354
    {
355 12
        $thisFile = $this->fs->getRelativePath($this->parentConfig);
356 12
        $errorMsg = '';
357
358 12
        if ($this->fs->isDir($filePath))
359 12
        {
360 1
            $errorMsg = 'a directory';
361 1
        }
362 11
        elseif ($this->fs->isSymlink($filePath))
363
        {
364 1
            $errorMsg = 'a symbolically linked file';
365 1
        }
366 10
        elseif ($this->fs->absolutePath($this->currentFile) == $this->fs->absolutePath($filePath))
367
        {
368 1
            $errorMsg = 'yourself';
369 1
        }
370 9
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
371
        {
372 1
            $errorMsg = 'a non-YAML configuration';
373 1
        }
374
375 12
        if (!($noErrors = empty($errorMsg)))
376 12
        {
377 4
            $this->output->error("{file}: you can't import {message}: {import}", array(
378 4
                'file' => $thisFile,
379 4
                'message' => $errorMsg,
380
                'import' => $filePath
381 4
            ));
382 4
        }
383
384 12
        return $noErrors;
385
    }
386
387 32
    private function isRecursiveImport($filePath)
388
    {
389 32
        if (in_array($filePath, self::$configImports))
390 32
        {
391 1
            throw new RecursiveConfigurationException($filePath, sprintf(
392 1
                'The %s file has already been imported', $filePath
393 1
            ));
394
        }
395
396 32
        self::$configImports[] = $filePath;
397 32
    }
398
399
    /**
400
     * Merge the given array with existing configuration
401
     *
402
     * @param  array $importedConfig
403
     * @param  array $existingConfig
404
     *
405
     * @return array
406
     */
407 7
    private function mergeImports(array $importedConfig, array $existingConfig)
408
    {
409 7
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $existingConfig, false);
410 7
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
411 7
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
412
413 7
        return $result;
414
    }
415
}
416