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

Configuration::getRedirectTemplate()   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
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 0
cts 2
cp 0
crap 2
rs 10
c 0
b 0
f 0
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 45
    private static function readFile($filePath)
70
    {
71 45
        $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 45
        $fileRaw = $fs->safeReadFile($filePath);
73 45
        $parsed = Yaml::parse($fileRaw);
74
75 45
        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
        $this->currentFile = $configFile;
103
104
        try
105
        {
106 45
            $this->isRecursiveImport($configFile);
107
108 45
            $parsedConfig = self::readFile($configFile);
109
110 45
            $this->handleImports($parsedConfig);
111
112 45
            unset($parsedConfig[self::IMPORT_KEYWORD]);
113 45
            return $parsedConfig;
114
        }
115 2
        catch (ParseException $e)
116
        {
117
            $this->output->error('{file}: parsing failed... {message}', array(
118
                'message' => $e->getMessage(),
119
                'file' => $configFile,
120
            ));
121
            $this->output->error('Using default configuration...');
122
        }
123 2
        catch (RecursiveConfigurationException $e)
124
        {
125 1
            $this->output->error("{file}: you can't recursively import a file that's already been imported: {import}", array(
126 1
                'file' => $configFile,
127 1
                'import' => $e->getRecursiveImport(),
128
            ));
129
        }
130
131 1
        return array();
132
    }
133
134
    /**
135
     * {@inheritdoc}
136
     */
137 45
    public function setLogger(LoggerInterface $logger)
138
    {
139 45
        $this->output = $logger;
140 45
    }
141
142 14
    public function isDebug()
143
    {
144 14
        return $this->returnConfigOption('debug', false);
145
    }
146
147
    /**
148
     * @TODO 1.0.0 Remove support for 'base' in next major release; it has been replaced by 'baseurl'
149
     *
150
     * @return mixed|null
151
     */
152 4
    public function getBaseUrl()
153
    {
154 4
        $base = $this->returnConfigOption('base');
155 4
        $baseUrl = $this->returnConfigOption('baseurl');
156
157 4
        if (is_null($base) || (!empty($baseUrl)))
158
        {
159 3
            return $baseUrl;
160
        }
161
162 1
        return $base;
163
    }
164
165
    /**
166
     * @return string[]
167
     */
168 1
    public function getDataFolders()
169
    {
170 1
        return $this->returnConfigOption('data');
171
    }
172
173
    /**
174
     * @return string[]
175
     */
176 1
    public function getDataSets()
177
    {
178 1
        return $this->returnConfigOption('datasets');
179
    }
180
181 1
    public function getIncludes()
182
    {
183 1
        return $this->returnConfigOption('include', array());
184
    }
185
186 1
    public function getExcludes()
187
    {
188 1
        return $this->returnConfigOption('exclude', array());
189
    }
190
191 14
    public function getTheme()
192
    {
193 14
        return $this->returnConfigOption('theme');
194
    }
195
196 7
    public function getConfiguration()
197
    {
198 7
        return $this->configuration;
199
    }
200
201 1
    public function getPageViewFolders()
202
    {
203 1
        return $this->returnConfigOption('pageviews');
204
    }
205
206 2
    public function getTargetFolder()
207
    {
208 2
        return $this->returnConfigOption('target');
209
    }
210
211 1
    public function getCollectionsFolders()
212
    {
213 1
        return $this->returnConfigOption('collections');
214
    }
215
216 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...
217
    {
218 14
        return $this->configuration['twig']['autoescape'];
219
    }
220
221
    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...
222
    {
223
        return $this->configuration['templates']['redirect'];
224
    }
225
226
    /**
227
     * Return the specified configuration option if available, otherwise return the default.
228
     *
229
     * @param string     $name    The configuration option to lookup
230
     * @param mixed|null $default The default value returned if the configuration option isn't found
231
     *
232
     * @return mixed|null
233
     */
234 45
    private function returnConfigOption($name, $default = null)
235
    {
236 45
        return isset($this->configuration[$name]) ? $this->configuration[$name] : $default;
237
    }
238
239 45
    private function mergeDefaultConfiguration()
240
    {
241
        $defaultConfig = array(
242 45
            'baseurl'   => '',
243 45
            'target'    => '_site',
244
            'twig'      => array(
245
                'autoescape' => false,
246
            ),
247
            'include'   => array(
248
                '.htaccess',
249
            ),
250
            'exclude'   => array(
251 45
                'node_modules/',
252 45
                'stakx-theme.yml',
253 45
                self::DEFAULT_NAME,
254
            ),
255
            'templates' => array(
256
                'redirect' => false,
257
            ),
258
        );
259
260 45
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
261 45
    }
262
263 45
    private function handleDeprecations()
264
    {
265
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
266 45
        $base = $this->returnConfigOption('base');
267
268 45
        if (!is_null($base))
269
        {
270 2
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
271
        }
272 45
    }
273
274 45
    private function handleImports(&$configuration)
275
    {
276 45
        if (!array_key_exists(self::IMPORT_KEYWORD, $configuration))
277
        {
278 45
            $this->output->debug('{file}: does not import any other files', array(
279 45
                'file' => $this->parentConfig,
280
            ));
281
282 45
            return;
283
        }
284
285 16
        if (!is_array($imports = $configuration[self::IMPORT_KEYWORD]))
286
        {
287 4
            $this->output->error('{file}: the reserved "import" keyword can only be an array');
288
289 4
            return;
290
        }
291
292 12
        $thisFile = $this->fs->getRelativePath($this->parentConfig);
293 12
        $parentConfigLocation = $this->fs->getFolderPath($thisFile);
294
295 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...
296
        {
297 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...
298
            {
299
                $this->output->error('{file}: invalid import: {message}', array(
300
                    'file' => $thisFile,
301
                    '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...
302
                ));
303
304
                continue;
305
            }
306
307 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...
308
309 12
            if (!$this->isValidImport($import))
310
            {
311 4
                continue;
312
            }
313
314 8
            $this->output->debug('{file}: imports additional file: {import}', array(
315 8
                'file' => $thisFile,
316 8
                'import' => $import,
317
            ));
318
319
            try
320
            {
321 8
                $importedConfig = $this->parseConfig($import);
322 7
                $configuration = $this->mergeImports($importedConfig, $configuration);
323
            }
324 1
            catch (FileAccessDeniedException $e)
325
            {
326
                $this->output->warning('{file}: trying access file outside of project directory: {import}', array(
327
                    'file' => $thisFile,
328
                    'import' => $import,
329
                ));
330
            }
331 1
            catch (FileNotFoundException $e)
332
            {
333 1
                $this->output->warning('{file}: could not find file to import: {import}', array(
334 1
                    'file' => $thisFile,
335 1
                    'import' => $import,
336
                ));
337
            }
338
        }
339 12
    }
340
341
    /**
342
     * Check whether a given file path is a valid import
343
     *
344
     * @param  string $filePath
345
     *
346
     * @return bool
347
     */
348 12
    private function isValidImport($filePath)
349
    {
350 12
        $thisFile = $this->fs->getRelativePath($this->parentConfig);
351 12
        $errorMsg = '';
352
353 12
        if ($this->fs->isDir($filePath))
354
        {
355 1
            $errorMsg = 'a directory';
356
        }
357 11
        elseif ($this->fs->isSymlink($filePath))
358
        {
359 1
            $errorMsg = 'a symbolically linked file';
360
        }
361 10
        elseif ($this->fs->absolutePath($this->currentFile) == $this->fs->absolutePath($filePath))
362
        {
363 1
            $errorMsg = 'yourself';
364
        }
365 9
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
366
        {
367 1
            $errorMsg = 'a non-YAML configuration';
368
        }
369
370 12
        if (!($noErrors = empty($errorMsg)))
371
        {
372 4
            $this->output->error("{file}: you can't import {message}: {import}", array(
373 4
                'file' => $thisFile,
374 4
                'message' => $errorMsg,
375 4
                'import' => $filePath
376
            ));
377
        }
378
379 12
        return $noErrors;
380
    }
381
382 45
    private function isRecursiveImport($filePath)
383
    {
384 45
        if (in_array($filePath, self::$configImports))
385
        {
386 1
            throw new RecursiveConfigurationException($filePath, sprintf(
387 1
                'The %s file has already been imported', $filePath
388
            ));
389
        }
390
391 45
        self::$configImports[] = $filePath;
392 45
    }
393
394
    /**
395
     * Merge the given array with existing configuration
396
     *
397
     * @param  array $importedConfig
398
     * @param  array $existingConfig
399
     *
400
     * @return array
401
     */
402 7
    private function mergeImports(array $importedConfig, array $existingConfig)
403
    {
404 7
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $existingConfig, false);
405 7
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
406 7
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
407
408 7
        return $result;
409
    }
410
}
411