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

Configuration::mergeImports()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 5
nc 1
nop 1
dl 0
loc 8
ccs 0
cts 0
cp 0
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 30
     * @var Filesystem
50
     */
51 30
    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 30
53 30
    /**
54
     * Configuration constructor.
55
     */
56
    public function __construct()
57
    {
58
        $this->configuration = array();
59
        $this->fs = new Filesystem();
60
    }
61
62
    /**
63 30
     * Parse a given configuration file and configure this Configuration instance.
64
     *
65 30
     * 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 17
     * @param bool        $ignoreDefaults When set to true, the default configuration will not be merged
70
     */
71
    public function parseConfiguration($configFile = null, $ignoreDefaults = false)
72
    {
73
        $this->configFile = $configFile;
74
75
        if ($this->fs->exists($configFile))
76
        {
77
            $configContents = $this->fs->safeReadFile($this->configFile);
78
79
            try
80 30
            {
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 30
            }
83
            catch (ParseException $e)
84
            {
85
                $this->output->error('Parsing the configuration failed: {message}', array(
86
                    'message' => $e->getMessage(),
87 17
                ));
88
                $this->output->error('Using default configuration...');
89 17
            }
90 17
91
            $this->handleImports();
92 14
        }
93
94 14
        !$ignoreDefaults && $this->mergeDefaultConfiguration();
95
96
        $this->handleDeprecations();
97
    }
98
99
    /**
100
     * {@inheritdoc}
101
     */
102 4
    public function setLogger(LoggerInterface $logger)
103
    {
104 4
        $this->output = $logger;
105 4
    }
106
107 4
    public function isDebug()
108
    {
109 3
        return $this->returnConfigOption('debug', false);
110
    }
111
112 1
    /**
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
    public function getBaseUrl()
118 1
    {
119
        $base = $this->returnConfigOption('base');
120 1
        $baseUrl = $this->returnConfigOption('baseurl');
121
122
        if (is_null($base) || (!empty($baseUrl)))
123
        {
124
            return $baseUrl;
125
        }
126 1
127
        return $base;
128 1
    }
129
130
    /**
131 1
     * @return string[]
132
     */
133 1
    public function getDataFolders()
134
    {
135
        return $this->returnConfigOption('data');
136 1
    }
137
138 1
    /**
139
     * @return string[]
140
     */
141 14
    public function getDataSets()
142
    {
143 14
        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 3
    public function getTheme()
157
    {
158 3
        return $this->returnConfigOption('theme');
159
    }
160
161 1
    public function getConfiguration()
162
    {
163 1
        return $this->configuration;
164
    }
165
166 14
    public function getPageViewFolders()
167
    {
168 14
        return $this->returnConfigOption('pageviews');
169
    }
170
171
    public function getTargetFolder()
172
    {
173
        return $this->returnConfigOption('target');
174
    }
175
176
    public function getCollectionsFolders()
177
    {
178
        return $this->returnConfigOption('collections');
179
    }
180
181
    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
        return $this->configuration['twig']['autoescape'];
184 30
    }
185
186 30
    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 30
    }
190
191
    /**
192 30
     * Return the specified configuration option if available, otherwise return the default.
193 30
     *
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
    private function returnConfigOption($name, $default = null)
200
    {
201 30
        return isset($this->configuration[$name]) ? $this->configuration[$name] : $default;
202 30
    }
203 30
204
    private function mergeDefaultConfiguration()
205
    {
206
        $defaultConfig = array(
207
            'baseurl'   => '',
208
            'target'    => '_site',
209
            'twig'      => array(
210 30
                'autoescape' => false,
211 30
            ),
212
            'include'   => array(
213 30
                '.htaccess',
214
            ),
215
            'exclude'   => array(
216 30
                'node_modules/',
217
                'stakx-theme.yml',
218 30
                self::DEFAULT_NAME,
219
            ),
220 2
            'templates' => array(
221
                'redirect' => false,
222 30
            ),
223
        );
224
225
        $this->configuration = ArrayUtilities::array_merge_defaults($defaultConfig, $this->configuration, 'name');
226
    }
227
228
    private function handleDeprecations()
229
    {
230
        // @TODO 1.0.0 handle 'base' deprecation in _config.yml
231
        $base = $this->returnConfigOption('base');
232
233
        if (!is_null($base))
234
        {
235
            $this->output->warning("The 'base' configuration option has been replaced by 'baseurl' and will be removed in in version 1.0.0.");
236
        }
237
    }
238
239
    private function handleImports()
240
    {
241
        if (!isset($this->configuration[self::IMPORT_KEYWORD]))
242
        {
243
            $this->output->debug('{file}: does not import any other files', array(
244
                'file' => $this->configFile,
245
            ));
246
247
            return;
248
        }
249
250
        $thisFile = $this->fs->getRelativePath($this->configFile);
251
        $imports = $this->configuration[self::IMPORT_KEYWORD];
252
253
        foreach ($imports as $import)
254
        {
255
            if (!$this->configImportIsValid($import))
256
            {
257
                continue;
258
            }
259
260
            $this->output->debug('{file}: imports additional file: {import}', array(
261
                'file' => $thisFile,
262
                'import' => $import,
263
            ));
264
265
            try
266
            {
267
                $importedConfig = $this->parseOther($import)->getConfiguration();
268
                $this->mergeImports($importedConfig);
269
            }
270
            catch (FileNotFoundException $e)
271
            {
272
                $this->output->warning('{file}: could not import file: {import}', array(
273
                    'file' => $thisFile,
274
                    'import' => $import,
275
                ));
276
            }
277
        }
278
279
        unset($this->configuration[self::IMPORT_KEYWORD]);
280
    }
281
282
    /**
283
     * Check whether a given file path is a valid import
284
     *
285
     * @param  string $filePath
286
     *
287
     * @return bool
288
     */
289
    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...
290
    {
291
        $thisFile = $this->fs->getRelativePath($this->configFile);
292
        $errorMsg = '';
293
294
        if ($this->fs->isDir($filePath))
295
        {
296
            $errorMsg = 'a directory';
297
        }
298
        elseif ($this->fs->isSymlink($filePath))
299
        {
300
            $errorMsg = 'a symbolically linked file';
301
        }
302
        elseif ($this->fs->absolutePath($this->configFile) == $this->fs->absolutePath($filePath))
303
        {
304
            $errorMsg = 'a config recursively';
305
        }
306
        elseif (($ext = $this->fs->getExtension($filePath)) != 'yml' && $ext != 'yaml')
307
        {
308
            $errorMsg = 'a non-YAML configuration';
309
        }
310
311
        if (!($noErrors = empty($errorMsg)))
312
        {
313
            $this->output->error("{file}: you can't import {message}: {import}", array(
314
                'file' => $thisFile,
315
                'message' => $errorMsg,
316
                'import' => $filePath
317
            ));
318
        }
319
320
        return $noErrors;
321
    }
322
323
    /**
324
     * Import a given configuration to be merged with the parent configuration
325
     *
326
     * @param  string $filePath
327
     *
328
     * @return Configuration
329
     */
330
    private function parseOther($filePath)
331
    {
332
        $config = new self();
333
        $config->setLogger($this->output);
334
        $config->parseConfiguration($filePath, true);
335
336
        return $config;
337
    }
338
339
    /**
340
     * Merge the given array with existing configuration
341
     *
342
     * @param array $importedConfig
343
     */
344
    private function mergeImports(array $importedConfig)
345
    {
346
        $arraySplit = ArrayUtilities::associative_array_split(self::IMPORT_KEYWORD, $this->configuration, false);
347
        $beforeImport = ArrayUtilities::array_merge_defaults($arraySplit[0], $importedConfig, 'name');
348
        $result = ArrayUtilities::array_merge_defaults($beforeImport, $arraySplit[1], 'name');
349
350
        $this->configuration = $result;
351
    }
352
}
353