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

Configuration::isDebug()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
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\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 38
    public function __construct()
57
    {
58 38
        $this->configuration = array();
59 38
        $this->fs = new Filesystem();
60 38
    }
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 38
    public function parseConfiguration($configFile = null, $ignoreDefaults = false)
72
    {
73 38
        $this->configFile = $configFile;
74
75 38
        if ($this->fs->exists($configFile))
76 38
        {
77 25
            $configContents = $this->fs->safeReadFile($this->configFile);
78
79
            try
80
            {
81 25
                $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 25
            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 25
            $this->handleImports();
92 25
        }
93
94 38
        !$ignoreDefaults && $this->mergeDefaultConfiguration();
95
96 38
        $this->handleDeprecations();
97 38
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 25
    public function setLogger(LoggerInterface $logger)
103
    {
104 25
        $this->output = $logger;
105 25
    }
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 mixed|null
116
     */
117 4
    public function getBaseUrl()
118
    {
119 4
        $base = $this->returnConfigOption('base');
120 4
        $baseUrl = $this->returnConfigOption('baseurl');
121
122 4
        if (is_null($base) || (!empty($baseUrl)))
123 4
        {
124 3
            return $baseUrl;
125
        }
126
127 1
        return $base;
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 6
    public function getConfiguration()
162
    {
163 6
        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
    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...
187
    {
188
        return $this->configuration['templates']['redirect'];
189
    }
190
191
    /**
192
     * Return the specified configuration option if available, otherwise return the default.
193
     *
194
     * @param string     $name    The configuration option to lookup
195
     * @param mixed|null $default The default value returned if the configuration option isn't found
196
     *
197
     * @return mixed|null
198
     */
199 38
    private function returnConfigOption($name, $default = null)
200
    {
201 38
        return isset($this->configuration[$name]) ? $this->configuration[$name] : $default;
202
    }
203
204 38
    private function mergeDefaultConfiguration()
205
    {
206
        $defaultConfig = array(
207 38
            'baseurl'   => '',
208 38
            'target'    => '_site',
209
            'twig'      => array(
210 38
                'autoescape' => false,
211 38
            ),
212
            'include'   => array(
213 38
                '.htaccess',
214 38
            ),
215
            'exclude'   => array(
216 38
                'node_modules/',
217 38
                'stakx-theme.yml',
218 38
                self::DEFAULT_NAME,
219 38
            ),
220
            'templates' => array(
221 38
                'redirect' => false,
222 38
            ),
223 38
        );
224
225 38
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
226 38
    }
227
228 38
    private function handleDeprecations()
229
    {
230
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
231 38
        $base = $this->returnConfigOption('base');
232
233 38
        if (!is_null($base))
234 38
        {
235 2
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
236 2
        }
237 38
    }
238
239 25
    private function handleImports()
240
    {
241 25
        if (!isset($this->configuration[self::IMPORT_KEYWORD]))
242 25
        {
243 25
            $this->output->debug('{file}: does not import any other files', array(
244 25
                'file' => $this->configFile,
245 25
            ));
246
247 25
            return;
248
        }
249
250 8
        $thisFile = $this->fs->getRelativePath($this->configFile);
251 8
        $parentConfigLocation = $this->fs->getFolderPath($thisFile);
252 8
        $imports = $this->configuration[self::IMPORT_KEYWORD];
253
254 8
        if (!is_array($imports))
255 8
        {
256 3
            $this->output->error('{file}: the reserved "import" keyword can only be an array');
257 3
            return;
258
        }
259
260 5
        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...
261
        {
262 5
            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...
263 5
            {
264
                continue;
265
            }
266
267 5
            $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...
268
269 5
            if (!$this->configImportIsValid($import))
270 5
            {
271
                continue;
272
            }
273
274 5
            $this->output->debug('{file}: imports additional file: {import}', array(
275 5
                'file' => $thisFile,
276 5
                'import' => $import,
277 5
            ));
278
279
            try
280
            {
281 5
                $importedConfig = $this->parseOther($import)->getConfiguration();
282 5
                $this->mergeImports($importedConfig);
283
            }
284 5
            catch (FileNotFoundException $e)
285
            {
286
                $this->output->warning('{file}: could not import file: {import}', array(
287
                    'file' => $thisFile,
288
                    'import' => $import,
289
                ));
290
            }
291 5
        }
292
293 5
        unset($this->configuration[self::IMPORT_KEYWORD]);
294 5
    }
295
296
    /**
297
     * Check whether a given file path is a valid import
298
     *
299
     * @param  string $filePath
300
     *
301
     * @return bool
302
     */
303 5
    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...
304
    {
305 5
        $thisFile = $this->fs->getRelativePath($this->configFile);
306 5
        $errorMsg = '';
307
308 5
        if ($this->fs->isDir($filePath))
309 5
        {
310
            $errorMsg = 'a directory';
311
        }
312 5
        elseif ($this->fs->isSymlink($filePath))
313
        {
314
            $errorMsg = 'a symbolically linked file';
315
        }
316 5
        elseif ($this->fs->absolutePath($this->configFile) == $this->fs->absolutePath($filePath))
317
        {
318
            $errorMsg = 'a config recursively';
319
        }
320 5
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
321
        {
322
            $errorMsg = 'a non-YAML configuration';
323
        }
324
325 5
        if (!($noErrors = empty($errorMsg)))
326 5
        {
327
            $this->output->error("{file}: you can't import {message}: {import}", array(
328
                'file' => $thisFile,
329
                'message' => $errorMsg,
330
                'import' => $filePath
331
            ));
332
        }
333
334 5
        return $noErrors;
335
    }
336
337
    /**
338
     * Import a given configuration to be merged with the parent configuration
339
     *
340
     * @param  string $filePath
341
     *
342
     * @return Configuration
343
     */
344 5
    private function parseOther($filePath)
345
    {
346 5
        $config = new self();
347 5
        $config->setLogger($this->output);
348 5
        $config->parseConfiguration($filePath, true);
349
350 5
        return $config;
351
    }
352
353
    /**
354
     * Merge the given array with existing configuration
355
     *
356
     * @param array $importedConfig
357
     */
358 5
    private function mergeImports(array $importedConfig)
359
    {
360 5
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $this->configuration, false);
361 5
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
362 5
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
363
364 5
        $this->configuration = $result;
365 5
    }
366
}
367