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

Configuration::parseConfiguration()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 27
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 4.25

Importance

Changes 0
Metric Value
cc 4
eloc 13
nc 6
nop 2
dl 0
loc 27
ccs 12
cts 16
cp 0.75
crap 4.25
rs 8.5806
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\System\Filesystem;
11
use allejo\stakx\Utilities\ArrayUtilities;
12
use Psr\Log\LoggerAwareInterface;
13
use Psr\Log\LoggerInterface;
14
use Symfony\Component\Filesystem\Exception\FileNotFoundException;
15
use Symfony\Component\Yaml\Exception\ParseException;
16
use Symfony\Component\Yaml\Yaml;
17
18
class Configuration implements LoggerAwareInterface
19
{
20
    const DEFAULT_NAME = '_config.yml';
21
    const IMPORT_KEYWORD = 'import';
22
23
    /**
24
     * A list of regular expressions or files directly related to stakx websites that should not be copied over to the
25
     * compiled website as an asset.
26
     *
27
     * @var array
28
     */
29
    public static $stakxSourceFiles = array('/^_(?!themes).*/', '/.twig$/');
30
31
    /**
32
     * An array representation of the main Yaml configuration.
33
     *
34
     * @var array
35
     */
36
    private $configuration;
37
38
    /**
39
     * @var string
40
     */
41
    private $configFile;
42
43
    /**
44
     * @var LoggerInterface
45
     */
46
    private $output;
47
48
    /**
49
     * @var Filesystem
50
     */
51
    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...
52
53
    /**
54
     * Configuration constructor.
55
     */
56 27
    public function __construct()
57
    {
58 27
        $this->configuration = array();
59 27
        $this->fs = new Filesystem();
60 27
    }
61
62
    /**
63
     * Parse a given configuration file and configure this Configuration instance.
64
     *
65
     * This function should be called with 'null' passed when "configuration-less" mode is used
66
     *
67
     * @param string|null $configFile     The path to the configuration file. If null, the default configuration will be
68
     *                                    used
69
     * @param bool        $ignoreDefaults When set to true, the default configuration will not be merged
70
     */
71 27
    public function parseConfiguration($configFile = null, $ignoreDefaults = false)
72
    {
73 27
        $this->configFile = $configFile;
74
75 27
        if ($this->fs->exists($configFile))
76 27
        {
77 14
            $configContents = $this->fs->safeReadFile($this->configFile);
78
79
            try
80
            {
81 14
                $this->configuration = Yaml::parse($configContents);
0 ignored issues
show
Documentation Bug introduced by
It seems like \Symfony\Component\Yaml\...:parse($configContents) can also be of type string or object<stdClass>. However, the property $configuration is declared as type array. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
82
            }
83 14
            catch (ParseException $e)
84
            {
85
                $this->output->error('Parsing the configuration failed: {message}', array(
86
                    'message' => $e->getMessage(),
87
                ));
88
                $this->output->error('Using default configuration...');
89
            }
90
91 14
            $this->handleImports();
92 14
        }
93
94 27
        !$ignoreDefaults && $this->mergeDefaultConfiguration();
95
96 27
        $this->handleDeprecations();
97 27
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 14
    public function setLogger(LoggerInterface $logger)
103
    {
104 14
        $this->output = $logger;
105 14
    }
106
107 14
    public function isDebug()
108
    {
109 14
        return $this->returnConfigOption('debug', false);
110
    }
111
112
    /**
113
     * @TODO 1.0.0 Remove support for 'base' in next major release; it has been replaced by 'baseurl'
114
     *
115
     * @return string
116
     */
117 2
    public function getBaseUrl()
118
    {
119 2
        $base = $this->returnConfigOption('base');
120 2
        $baseUrl = $this->returnConfigOption('baseurl');
121
122 2
        if (is_null($base) || (!empty($baseUrl)))
123 2
        {
124 2
            return $baseUrl;
125
        }
126
127
        return $base;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $base; (object|integer|double|string|array|boolean) is incompatible with the return type documented by allejo\stakx\Configuration::getBaseUrl of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
128
    }
129
130
    /**
131
     * @return string[]
132
     */
133 1
    public function getDataFolders()
134
    {
135 1
        return $this->returnConfigOption('data');
136
    }
137
138
    /**
139
     * @return string[]
140
     */
141 1
    public function getDataSets()
142
    {
143 1
        return $this->returnConfigOption('datasets');
144
    }
145
146 1
    public function getIncludes()
147
    {
148 1
        return $this->returnConfigOption('include', array());
149
    }
150
151 1
    public function getExcludes()
152
    {
153 1
        return $this->returnConfigOption('exclude', array());
154
    }
155
156 14
    public function getTheme()
157
    {
158 14
        return $this->returnConfigOption('theme');
159
    }
160
161 1
    public function getConfiguration()
162
    {
163 1
        return $this->configuration;
164
    }
165
166 1
    public function getPageViewFolders()
167
    {
168 1
        return $this->returnConfigOption('pageviews');
169
    }
170
171 2
    public function getTargetFolder()
172
    {
173 2
        return $this->returnConfigOption('target');
174
    }
175
176 1
    public function getCollectionsFolders()
177
    {
178 1
        return $this->returnConfigOption('collections');
179
    }
180
181 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...
182
    {
183 14
        return $this->configuration['twig']['autoescape'];
184
    }
185
186
    /**
187
     * @return string|false
188
     */
189
    public function getRedirectTemplate()
190
    {
191
        return $this->configuration['templates']['redirect'];
192
    }
193
194
    /**
195
     * Return the specified configuration option if available, otherwise return the default.
196
     *
197
     * @param string     $name    The configuration option to lookup
198
     * @param mixed|null $default The default value returned if the configuration option isn't found
199
     *
200
     * @return mixed|null
201
     */
202 27
    private function returnConfigOption($name, $default = null)
203
    {
204 27
        return isset($this->configuration[$name]) ? $this->configuration[$name] : $default;
205
    }
206
207 27
    private function mergeDefaultConfiguration()
208
    {
209
        $defaultConfig = array(
210 27
            'baseurl'   => '',
211 27
            'target'    => '_site',
212
            'twig'      => array(
213 27
                'autoescape' => false,
214 27
            ),
215
            'include'   => array(
216 27
                '.htaccess',
217 27
            ),
218
            'exclude'   => array(
219 27
                'node_modules/',
220 27
                'stakx-theme.yml',
221 27
                self::DEFAULT_NAME,
222 27
            ),
223
            'templates' => array(
224 27
                'redirect' => false,
225 27
            ),
226 27
        );
227
228 27
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
229 27
    }
230
231 27
    private function handleDeprecations()
232
    {
233
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
234 27
        $base = $this->returnConfigOption('base');
235
236 27
        if (!is_null($base))
237 27
        {
238
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
239
        }
240 27
    }
241
242 14
    private function handleImports()
243
    {
244 14
        if (!isset($this->configuration[self::IMPORT_KEYWORD]))
245 14
        {
246 14
            $this->output->debug('{file}: does not import any other files', array(
247 14
                'file' => $this->configFile,
248 14
            ));
249
250 14
            return;
251
        }
252
253
        $thisFile = $this->fs->getRelativePath($this->configFile);
254
        $imports = $this->configuration[self::IMPORT_KEYWORD];
255
256
        foreach ($imports as $import)
257
        {
258
            if (!$this->configImportIsValid($import))
259
            {
260
                continue;
261
            }
262
263
            $this->output->debug('{file}: imports additional file: {import}', array(
264
                'file' => $thisFile,
265
                'import' => $import,
266
            ));
267
268
            try
269
            {
270
                $importedConfig = $this->parseOther($import)->getConfiguration();
271
                $this->mergeImports($importedConfig);
272
            }
273
            catch (FileNotFoundException $e)
274
            {
275
                $this->output->warning('{file}: could not import file: {import}', array(
276
                    'file' => $thisFile,
277
                    'import' => $import,
278
                ));
279
            }
280
        }
281
282
        unset($this->configuration[self::IMPORT_KEYWORD]);
283
    }
284
285
    /**
286
     * Check whether a given file path is a valid import
287
     *
288
     * @param  string $filePath
289
     *
290
     * @return bool
291
     */
292
    private function configImportIsValid($filePath)
0 ignored issues
show
Coding Style introduced by
function configImportIsValid() does not seem to conform to the naming convention (^(?:is|has|should|may|supports)).

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...
293
    {
294
        $thisFile = $this->fs->getRelativePath($this->configFile);
295
        $errorMsg = '';
296
297
        if ($this->fs->isDir($filePath))
298
        {
299
            $errorMsg = 'a directory';
300
        }
301
        elseif ($this->fs->isSymlink($filePath))
302
        {
303
            $errorMsg = 'a symbolically linked file';
304
        }
305
        elseif ($this->fs->absolutePath($this->configFile) == $this->fs->absolutePath($filePath))
306
        {
307
            $errorMsg = 'a config recursively';
308
        }
309
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
310
        {
311
            $errorMsg = 'a non-YAML configuration';
312
        }
313
314
        if (!($noErrors = empty($errorMsg)))
315
        {
316
            $this->output->error("{file}: you can't import {message}: {import}", array(
317
                'file' => $thisFile,
318
                'message' => $errorMsg,
319
                'import' => $filePath
320
            ));
321
        }
322
323
        return $noErrors;
324
    }
325
326
    /**
327
     * Import a given configuration to be merged with the parent configuration
328
     *
329
     * @param  string $filePath
330
     *
331
     * @return Configuration
332
     */
333
    private function parseOther($filePath)
334
    {
335
        $config = new self();
336
        $config->setLogger($this->output);
337
        $config->parseConfiguration($filePath, true);
338
339
        return $config;
340
    }
341
342
    /**
343
     * Merge the given array with existing configuration
344
     *
345
     * @param array $importedConfig
346
     */
347
    private function mergeImports(array $importedConfig)
348
    {
349
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $this->configuration, false);
350
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
351
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
352
353
        $this->configuration = $result;
354
    }
355
}
356