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

Configuration::handleDeprecations()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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