Completed
Pull Request — master (#58)
by Peter
08:32
created

Configuration   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 97.52%

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 5
dl 0
loc 406
ccs 157
cts 161
cp 0.9752
rs 3.36
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A getConfigTreeBuilder() 0 29 1
A getDatabaseNode() 0 31 1
A createTreeBuilder() 0 10 2
A getRootNode() 0 18 3
A arrayPrototype() 0 18 3
A normalizeDefaultDatabase() 0 18 4
A normalizeRootConfigurationToDefaultDatabase() 0 20 5
B normalizeLicenseDirtyHack() 0 18 7
A validateAvailableDefaultDatabase() 0 17 4
B allowGlobalLicense() 0 21 6
B allowGlobalLocales() 0 21 6
B validateDatabases() 0 33 9
A normalizeUrl() 0 17 4
A normalizePath() 0 13 3
A validateURL() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like Configuration often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Configuration, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * GpsLab component.
6
 *
7
 * @author    Peter Gribanov <[email protected]>
8
 * @copyright Copyright (c) 2017, Peter Gribanov
9
 * @license   http://opensource.org/licenses/MIT
10
 */
11
12
namespace GpsLab\Bundle\GeoIP2Bundle\DependencyInjection;
13
14
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
15
use Symfony\Component\Config\Definition\Builder\NodeDefinition;
16
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
17
use Symfony\Component\Config\Definition\ConfigurationInterface;
18
19
class Configuration implements ConfigurationInterface
20
{
21
    private const URL = 'https://download.maxmind.com/app/geoip_download?edition_id=%s&license_key=%s&suffix=tar.gz';
22
23
    private const PATH = '%s/%s.mmdb';
24
25
    private const LICENSE_DIRTY_HACK = 'YOUR-LICENSE-KEY';
26
27
    /**
28
     * @var string
29
     */
30
    private $cache_dir;
31
32
    /**
33 64
     * @param string|null $cache_dir
34
     */
35 64
    public function __construct(?string $cache_dir)
36 64
    {
37
        $this->cache_dir = $cache_dir ?: sys_get_temp_dir();
38
    }
39
40
    /**
41 64
     * @return TreeBuilder
42
     */
43 64
    public function getConfigTreeBuilder(): TreeBuilder
44 64
    {
45
        $tree_builder = $this->createTreeBuilder('gpslab_geoip');
46 64
        $root_node = $this->getRootNode($tree_builder, 'gpslab_geoip');
47 64
48 64
        $this->normalizeDefaultDatabase($root_node);
49 64
        $this->normalizeRootConfigurationToDefaultDatabase($root_node);
50 64
        $this->normalizeLicenseDirtyHack($root_node);
51 64
        $this->validateAvailableDefaultDatabase($root_node);
52
        $this->allowGlobalLicense($root_node);
53 64
        $this->allowGlobalLocales($root_node);
54 64
        $this->validateDatabases($root_node);
55 64
56 64
        $root_node->fixXmlConfig('locale');
57 64
        $locales = $root_node->children()->arrayNode('locales');
58
        $locales->prototype('scalar');
59 64
        $locales->treatNullLike([]);
60
        $locales->defaultValue(['en']);
61 64
62 64
        $root_node->children()->scalarNode('license');
63
64 64
        $default_database = $root_node->children()->scalarNode('default_database');
65 64
        $default_database->defaultValue('default');
66
67 64
        $root_node->fixXmlConfig('database');
68
        $root_node->append($this->getDatabaseNode());
69
70
        return $tree_builder;
71
    }
72
73 64
    /**
74
     * @return ArrayNodeDefinition
75 64
     */
76 64
    private function getDatabaseNode(): ArrayNodeDefinition
77 64
    {
78
        $tree_builder = $this->createTreeBuilder('databases');
79 64
        $root_node = $this->getRootNode($tree_builder, 'databases');
80
        $root_node->useAttributeAsKey('name');
81 64
82 64
        $database_node = $this->arrayPrototype($root_node);
83
84 64
        $this->normalizeUrl($database_node);
85 64
        $this->normalizePath($database_node);
86
87 64
        $url = $database_node->children()->scalarNode('url');
88
        $url->isRequired();
89 64
90 64
        $this->validateURL($url);
91
92 64
        $path = $database_node->children()->scalarNode('path');
93 64
        $path->isRequired();
94 64
95 64
        $database_node->fixXmlConfig('locale');
96 64
        $locales = $database_node->children()->arrayNode('locales');
97
        $locales->prototype('scalar');
98 64
        $locales->treatNullLike([]);
99
        $locales->defaultValue(['en']);
100 64
101
        $database_node->children()->scalarNode('license');
102 64
103
        $database_node->children()->enumNode('edition')->values(['GeoLite2-ASN', 'GeoLite2-City', 'GeoLite2-Country']);
104
105
        return $root_node;
106
    }
107
108
    /**
109
     * @param string $name
110 64
     *
111
     * @return TreeBuilder
112
     */
113 64
    private function createTreeBuilder(string $name): TreeBuilder
114
    {
115
        // Symfony 4.2 +
116
        if (method_exists(TreeBuilder::class, '__construct')) {
117
            return new TreeBuilder($name);
118 64
        }
119
120
        // Symfony 4.1 and below
121
        return new TreeBuilder();
0 ignored issues
show
Bug introduced by
The call to TreeBuilder::__construct() misses a required argument $name.

This check looks for function calls that miss required arguments.

Loading history...
122
    }
123
124
    /**
125
     * @param TreeBuilder $tree_builder
126
     * @param string      $name
127 64
     *
128
     * @return ArrayNodeDefinition
129 64
     */
130
    private function getRootNode(TreeBuilder $tree_builder, string $name): ArrayNodeDefinition
131
    {
132
        if (method_exists($tree_builder, 'getRootNode')) {
133
            // Symfony 4.2 +
134 64
            $root = $tree_builder->getRootNode();
135
        } else {
136
            // Symfony 4.1 and below
137
            $root = $tree_builder->root($name);
0 ignored issues
show
Bug introduced by
The method root() does not seem to exist on object<Symfony\Component...on\Builder\TreeBuilder>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
138
        }
139
140
        // @codeCoverageIgnoreStart
141
        if (!($root instanceof ArrayNodeDefinition)) { // should be always false
142
            throw new \RuntimeException(sprintf('The root node should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($root)));
143 64
        }
144
        // @codeCoverageIgnoreEnd
145
146
        return $root;
147
    }
148
149
    /**
150
     * @param ArrayNodeDefinition $root_node
151 64
     *
152
     * @return ArrayNodeDefinition
153
     */
154 64
    private function arrayPrototype(ArrayNodeDefinition $root_node): ArrayNodeDefinition
155 64
    {
156
        // Symfony 3.3 +
157
        if (method_exists($root_node, 'arrayPrototype')) {
158
            return $root_node->arrayPrototype();
159
        }
160
161
        // Symfony 3.2 and below
162
        $node = $root_node->prototype('array');
163
164
        // @codeCoverageIgnoreStart
165
        if (!($node instanceof ArrayNodeDefinition)) { // should be always false
166
            throw new \RuntimeException(sprintf('The "array" prototype should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($node)));
167
        }
168
        // @codeCoverageIgnoreEnd
169
170
        return $node;
171
    }
172
173
    /**
174
     * Normalize default_database from databases.
175 64
     *
176
     * @param NodeDefinition $root_node
177
     */
178 64
    private function normalizeDefaultDatabase(NodeDefinition $root_node): void
179
    {
180
        $root_node
181 59
            ->beforeNormalization()
182 59
            ->ifTrue(static function ($v): bool {
183 59
                return
184 59
                    is_array($v) &&
185 64
                    !array_key_exists('default_database', $v) &&
186
                    !empty($v['databases']) &&
187 20
                    is_array($v['databases']);
188 20
            })
189
            ->then(static function (array $v): array {
190 20
                $keys = array_keys($v['databases']);
191 64
                $v['default_database'] = reset($keys);
192 64
193
                return $v;
194
            });
195
    }
196
197
    /**
198
     * Normalize databases root configuration to default_database.
199 64
     *
200
     * @param NodeDefinition $root_node
201
     */
202 64
    private function normalizeRootConfigurationToDefaultDatabase(NodeDefinition $root_node): void
203
    {
204 59
        $root_node
205 64
            ->beforeNormalization()
206
            ->ifTrue(static function ($v): bool {
207 20
                return $v && is_array($v) && !array_key_exists('databases', $v) && !array_key_exists('database', $v);
208 20
            })
209 20
            ->then(static function (array $v): array {
210
                $database = $v;
211
                unset($database['default_database']);
212 20
                $default_database = isset($v['default_database']) ? (string) $v['default_database'] : 'default';
213
214 20
                return [
215
                    'default_database' => $default_database,
216
                    'databases' => [
217 64
                        $default_database => $database,
218 64
                    ],
219
                ];
220
            });
221
    }
222
223
    /**
224
     * Dirty hack for Symfony Flex.
225 64
     * https://github.com/symfony/recipes-contrib/pull/837
226
     *
227
     * @param NodeDefinition $root_node
228 64
     */
229
    private function normalizeLicenseDirtyHack(NodeDefinition $root_node): void
230
    {
231 48
        $root_node
232 48
            ->beforeNormalization()
233 48
            ->ifTrue(static function ($v): bool {
234 48
                return $v && is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
235 64
            })
236
            ->then(static function (array $v): array {
237 4
                foreach ($v['databases'] as $name => $database) {
238
                    if (isset($database['license']) && $database['license'] === self::LICENSE_DIRTY_HACK) {
239 4
                        unset($v['databases'][$name]);
240 64
                        @trigger_error(sprintf('License for downloaded database "%s" is not specified.', $name), E_USER_WARNING);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
241 64
                    }
242
                }
243
244
                return $v;
245
            });
246
    }
247
248
    /**
249 64
     * Validate that the default_database exists in the list of databases.
250
     *
251
     * @param NodeDefinition $root_node
252 64
     */
253
    private function validateAvailableDefaultDatabase(NodeDefinition $root_node): void
254
    {
255 59
        $root_node
256 59
            ->validate()
257 59
            ->ifTrue(static function ($v): bool {
258 59
                return
259 64
                    is_array($v) &&
260
                    array_key_exists('default_database', $v) &&
261 6
                    !empty($v['databases']) &&
262 4
                    !array_key_exists($v['default_database'], $v['databases']);
263 4
            })
264
            ->then(static function (array $v): array {
265
                $databases = implode('", "', array_keys($v['databases']));
266
267 6
                throw new \InvalidArgumentException(sprintf('Undefined default database "%s". Available "%s" databases.', $v['default_database'], $databases));
268 64
            });
269 64
    }
270
271
    /**
272
     * Add a license option to the databases configuration if it does not exist.
273
     * Allow use a global license for all databases.
274
     *
275
     * @param NodeDefinition $root_node
276
     */
277 64
    private function allowGlobalLicense(NodeDefinition $root_node): void
278
    {
279
        $root_node
280 64
            ->beforeNormalization()
281
            ->ifTrue(static function ($v): bool {
282
                return
283 59
                    is_array($v) &&
284 59
                    array_key_exists('license', $v) &&
285 59
                    array_key_exists('databases', $v) &&
286 59
                    is_array($v['databases']);
287 64
            })
288
            ->then(static function (array $v): array {
289 2
                foreach ($v['databases'] as $name => $database) {
290 2
                    if (!array_key_exists('license', $database)) {
291 2
                        $v['databases'][$name]['license'] = $v['license'];
292
                    }
293
                }
294
295 2
                return $v;
296 64
            });
297 64
    }
298
299
    /**
300
     * Add a locales option to the databases configuration if it does not exist.
301
     * Allow use a global locales for all databases.
302
     *
303
     * @param NodeDefinition $root_node
304 64
     */
305
    private function allowGlobalLocales(NodeDefinition $root_node): void
306
    {
307 64
        $root_node
308
            ->beforeNormalization()
309 44
            ->ifTrue(static function ($v): bool {
310 64
                return
311
                    is_array($v) &&
312 44
                    array_key_exists('locales', $v) &&
313 31
                    array_key_exists('databases', $v) &&
314 8
                    is_array($v['databases']);
315
            })
316
            ->then(static function (array $v): array {
317 23
                foreach ($v['databases'] as $name => $database) {
318 4
                    if (!array_key_exists('locales', $database)) {
319
                        $v['databases'][$name]['locales'] = $v['locales'];
320
                    }
321 19
                }
322 4
323
                return $v;
324
            });
325 15
    }
326 4
327
    /**
328
     * Validate database options.
329 11
     *
330 11
     * @param NodeDefinition $root_node
331
     */
332
    private function validateDatabases(NodeDefinition $root_node): void
333
    {
334 24
        $root_node
335 64
            ->validate()
336 64
            ->ifTrue(static function ($v): bool {
337
                return is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
338
            })
339
            ->then(static function (array $v): array {
340
                foreach ($v['databases'] as $name => $database) {
341
                    if (empty($database['license'])) {
342
                        throw new \InvalidArgumentException(sprintf('License for downloaded database "%s" is not specified.', $name));
343 64
                    }
344
345
                    if (empty($database['edition'])) {
346 64
                        throw new \InvalidArgumentException(sprintf('Edition of downloaded database "%s" is not selected.', $name));
347
                    }
348
349 49
                    if (empty($database['url'])) {
350 49
                        throw new \InvalidArgumentException(sprintf('URL for download database "%s" is not specified.', $name));
351 49
                    }
352 49
353 64
                    if (empty($database['path'])) {
354
                        throw new \InvalidArgumentException(sprintf('The destination path to download database "%s" is not specified.', $name));
355 17
                    }
356
357 17
                    if (empty($database['locales'])) {
358 64
                        throw new \InvalidArgumentException(sprintf('The list of locales for database "%s" should not be empty.', $name));
359 64
                    }
360
                }
361
362
                return $v;
363
            });
364
    }
365
366 64
    /**
367
     * Normalize url option from license key and edition id.
368
     *
369 64
     * @param NodeDefinition $database_node
370
     */
371 49
    private function normalizeUrl(NodeDefinition $database_node): void
372 64
    {
373
        $database_node
374 17
            ->beforeNormalization()
375
            ->ifTrue(static function ($v): bool {
376 17
                return
377 64
                    is_array($v) &&
378 64
                    !array_key_exists('url', $v) &&
379
                    array_key_exists('license', $v) &&
380
                    array_key_exists('edition', $v);
381
            })
382
            ->then(static function (array $v): array {
383
                $v['url'] = sprintf(self::URL, urlencode($v['edition']), urlencode($v['license']));
384
385 64
                return $v;
386
            });
387
    }
388 64
389
    /**
390 43
     * Normalize path option from edition id.
391 64
     *
392
     * @param NodeDefinition $database_node
393 2
     */
394 64
    private function normalizePath(NodeDefinition $database_node): void
395 64
    {
396
        $database_node
397
            ->beforeNormalization()
398
            ->ifTrue(static function ($v): bool {
399
                return is_array($v) && !array_key_exists('path', $v) && array_key_exists('edition', $v);
400
            })
401
            ->then(function (array $v): array {
402
                $v['path'] = sprintf(self::PATH, $this->cache_dir, $v['edition']);
403
404
                return $v;
405
            });
406
    }
407
408
    /**
409
     * The url option must be a valid URL.
410
     *
411
     * @param NodeDefinition $url
412
     */
413
    private function validateURL(NodeDefinition $url): void
414
    {
415
        $url
416
            ->validate()
417
            ->ifTrue(static function ($v): bool {
418
                return is_string($v) && $v && !filter_var($v, FILTER_VALIDATE_URL);
419
            })
420
            ->then(static function (string $v): array {
421
                throw new \InvalidArgumentException(sprintf('URL "%s" must be valid.', $v));
422
            });
423
    }
424
}
425