Completed
Pull Request — master (#71)
by Peter
17:14 queued 08:34
created

Configuration   F

Complexity

Total Complexity 63

Size/Duplication

Total Lines 418
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.27%

Importance

Changes 0
Metric Value
wmc 63
lcom 1
cbo 5
dl 0
loc 418
ccs 170
cts 173
cp 0.9827
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
    private const DATABASE_EDITION_IDS = [
28
        'GeoLite2-ASN',
29
        'GeoLite2-City',
30
        'GeoLite2-Country',
31
        'GeoIP2-City',
32
        'GeoIP2-Country',
33
        'GeoIP2-Anonymous-IP',
34
        'GeoIP2-Domain',
35
        'GeoIP2-ISP',
36
    ];
37
38
    /**
39
     * @var string
40
     */
41
    private $cache_dir;
42
43
    /**
44
     * @param string|null $cache_dir
45
     */
46 90
    public function __construct(?string $cache_dir)
47
    {
48 90
        $this->cache_dir = $cache_dir ?: sys_get_temp_dir();
49 90
    }
50
51
    /**
52
     * @return TreeBuilder
53
     */
54 90
    public function getConfigTreeBuilder(): TreeBuilder
55
    {
56 90
        $tree_builder = $this->createTreeBuilder('gpslab_geoip');
57 90
        $root_node = $this->getRootNode($tree_builder, 'gpslab_geoip');
58
59 90
        $this->normalizeDefaultDatabase($root_node);
60 90
        $this->normalizeRootConfigurationToDefaultDatabase($root_node);
61 90
        $this->normalizeLicenseDirtyHack($root_node);
62 90
        $this->validateAvailableDefaultDatabase($root_node);
63 90
        $this->allowGlobalLicense($root_node);
64 90
        $this->allowGlobalLocales($root_node);
65 90
        $this->validateDatabases($root_node);
66
67 90
        $root_node->fixXmlConfig('locale');
68 90
        $locales = $root_node->children()->arrayNode('locales');
69 90
        $locales->prototype('scalar');
70 90
        $locales->treatNullLike([]);
71 90
        $locales->defaultValue(['en']);
72
73 90
        $root_node->children()->scalarNode('license');
74
75 90
        $default_database = $root_node->children()->scalarNode('default_database');
76 90
        $default_database->defaultValue('default');
77
78 90
        $root_node->fixXmlConfig('database');
79 90
        $root_node->append($this->getDatabaseNode());
80
81 90
        return $tree_builder;
82
    }
83
84
    /**
85
     * @return ArrayNodeDefinition
86
     */
87 90
    private function getDatabaseNode(): ArrayNodeDefinition
88
    {
89 90
        $tree_builder = $this->createTreeBuilder('databases');
90 90
        $root_node = $this->getRootNode($tree_builder, 'databases');
91 90
        $root_node->useAttributeAsKey('name');
92
93 90
        $database_node = $this->arrayPrototype($root_node);
94
95 90
        $this->normalizeUrl($database_node);
96 90
        $this->normalizePath($database_node);
97
98 90
        $url = $database_node->children()->scalarNode('url');
99 90
        $url->isRequired();
100
101 90
        $this->validateURL($url);
102
103 90
        $path = $database_node->children()->scalarNode('path');
104 90
        $path->isRequired();
105
106 90
        $database_node->fixXmlConfig('locale');
107 90
        $locales = $database_node->children()->arrayNode('locales');
108 90
        $locales->prototype('scalar');
109 90
        $locales->treatNullLike([]);
110 90
        $locales->defaultValue(['en']);
111
112 90
        $database_node->children()->scalarNode('license');
113
114 90
        $database_node->children()->enumNode('edition')->values(self::DATABASE_EDITION_IDS);
115
116 90
        return $root_node;
117
    }
118
119
    /**
120
     * @param string $name
121
     *
122
     * @return TreeBuilder
123
     */
124 90
    private function createTreeBuilder(string $name): TreeBuilder
125
    {
126
        // Symfony 4.2 +
127 90
        if (method_exists(TreeBuilder::class, '__construct')) {
128
            return new TreeBuilder($name);
129
        }
130
131
        // Symfony 4.1 and below
132 90
        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...
133
    }
134
135
    /**
136
     * @param TreeBuilder $tree_builder
137
     * @param string      $name
138
     *
139
     * @return ArrayNodeDefinition
140
     */
141 90
    private function getRootNode(TreeBuilder $tree_builder, string $name): ArrayNodeDefinition
142
    {
143 90
        if (method_exists($tree_builder, 'getRootNode')) {
144
            // Symfony 4.2 +
145
            $root = $tree_builder->getRootNode();
146
        } else {
147
            // Symfony 4.1 and below
148 90
            $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...
149
        }
150
151
        // @codeCoverageIgnoreStart
152
        if (!($root instanceof ArrayNodeDefinition)) { // should be always false
153
            throw new \RuntimeException(sprintf('The root node should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($root)));
154
        }
155
        // @codeCoverageIgnoreEnd
156
157 90
        return $root;
158
    }
159
160
    /**
161
     * @param ArrayNodeDefinition $root_node
162
     *
163
     * @return ArrayNodeDefinition
164
     */
165 90
    private function arrayPrototype(ArrayNodeDefinition $root_node): ArrayNodeDefinition
166
    {
167
        // Symfony 3.3 +
168 90
        if (method_exists($root_node, 'arrayPrototype')) {
169
            return $root_node->arrayPrototype();
170
        }
171
172
        // Symfony 3.2 and below
173 90
        $node = $root_node->prototype('array');
174
175
        // @codeCoverageIgnoreStart
176
        if (!($node instanceof ArrayNodeDefinition)) { // should be always false
177
            throw new \RuntimeException(sprintf('The "array" prototype should be instance of %s, got %s instead.', ArrayNodeDefinition::class, get_class($node)));
178
        }
179
        // @codeCoverageIgnoreEnd
180
181 90
        return $node;
182
    }
183
184
    /**
185
     * Normalize default_database from databases.
186
     *
187
     * @param NodeDefinition $root_node
188
     */
189 90
    private function normalizeDefaultDatabase(NodeDefinition $root_node): void
190
    {
191
        $root_node
192 90
            ->beforeNormalization()
193
            ->ifTrue(static function ($v): bool {
194
                return
195 88
                    is_array($v) &&
196 88
                    !array_key_exists('default_database', $v) &&
197 88
                    !empty($v['databases']) &&
198 88
                    is_array($v['databases']);
199 90
            })
200
            ->then(static function (array $v): array {
201 36
                $keys = array_keys($v['databases']);
202 36
                $v['default_database'] = reset($keys);
203
204 36
                return $v;
205 90
            });
206 90
    }
207
208
    /**
209
     * Normalize databases root configuration to default_database.
210
     *
211
     * @param NodeDefinition $root_node
212
     */
213 90
    private function normalizeRootConfigurationToDefaultDatabase(NodeDefinition $root_node): void
214
    {
215
        $root_node
216 90
            ->beforeNormalization()
217
            ->ifTrue(static function ($v): bool {
218 88
                return $v && is_array($v) && !array_key_exists('databases', $v) && !array_key_exists('database', $v);
219 90
            })
220
            ->then(static function (array $v): array {
221 36
                $database = $v;
222 36
                unset($database['default_database']);
223 36
                $default_database = isset($v['default_database']) ? (string) $v['default_database'] : 'default';
224
225
                return [
226 36
                    'default_database' => $default_database,
227
                    'databases' => [
228 36
                        $default_database => $database,
229
                    ],
230
                ];
231 90
            });
232 90
    }
233
234
    /**
235
     * Dirty hack for Symfony Flex.
236
     *
237
     * @see https://github.com/symfony/recipes-contrib/pull/837
238
     *
239
     * @param NodeDefinition $root_node
240
     */
241 90
    private function normalizeLicenseDirtyHack(NodeDefinition $root_node): void
242
    {
243
        $root_node
244 90
            ->beforeNormalization()
245
            ->ifTrue(static function ($v): bool {
246 88
                return $v && is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
247 90
            })
248
            ->then(static function (array $v): array {
249 82
                foreach ($v['databases'] as $name => $database) {
250 78
                    if (isset($database['license']) && $database['license'] === self::LICENSE_DIRTY_HACK) {
251 4
                        unset($v['databases'][$name]);
252 78
                        @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...
253
                    }
254
                }
255
256 82
                return $v;
257 90
            });
258 90
    }
259
260
    /**
261
     * Validate that the default_database exists in the list of databases.
262
     *
263
     * @param NodeDefinition $root_node
264
     */
265 90
    private function validateAvailableDefaultDatabase(NodeDefinition $root_node): void
266
    {
267
        $root_node
268 90
            ->validate()
269
            ->ifTrue(static function ($v): bool {
270
                return
271 74
                    is_array($v) &&
272 74
                    array_key_exists('default_database', $v) &&
273 74
                    !empty($v['databases']) &&
274 74
                    !array_key_exists($v['default_database'], $v['databases']);
275 90
            })
276
            ->then(static function (array $v): array {
277 4
                $databases = implode('", "', array_keys($v['databases']));
278
279 4
                throw new \InvalidArgumentException(sprintf('Undefined default database "%s". Available "%s" databases.', $v['default_database'], $databases));
280 90
            });
281 90
    }
282
283
    /**
284
     * Add a license option to the databases configuration if it does not exist.
285
     * Allow use a global license for all databases.
286
     *
287
     * @param NodeDefinition $root_node
288
     */
289 90
    private function allowGlobalLicense(NodeDefinition $root_node): void
290
    {
291
        $root_node
292 90
            ->beforeNormalization()
293
            ->ifTrue(static function ($v): bool {
294
                return
295 88
                    is_array($v) &&
296 88
                    array_key_exists('license', $v) &&
297 88
                    array_key_exists('databases', $v) &&
298 88
                    is_array($v['databases']);
299 90
            })
300
            ->then(static function (array $v): array {
301 20
                foreach ($v['databases'] as $name => $database) {
302 18
                    if (!array_key_exists('license', $database)) {
303 18
                        $v['databases'][$name]['license'] = $v['license'];
304
                    }
305
                }
306
307 20
                return $v;
308 90
            });
309 90
    }
310
311
    /**
312
     * Add a locales option to the databases configuration if it does not exist.
313
     * Allow use a global locales for all databases.
314
     *
315
     * @param NodeDefinition $root_node
316
     */
317 90
    private function allowGlobalLocales(NodeDefinition $root_node): void
318
    {
319
        $root_node
320 90
            ->beforeNormalization()
321
            ->ifTrue(static function ($v): bool {
322
                return
323 88
                    is_array($v) &&
324 88
                    array_key_exists('locales', $v) &&
325 88
                    array_key_exists('databases', $v) &&
326 88
                    is_array($v['databases']);
327 90
            })
328
            ->then(static function (array $v): array {
329 2
                foreach ($v['databases'] as $name => $database) {
330 2
                    if (!array_key_exists('locales', $database)) {
331 2
                        $v['databases'][$name]['locales'] = $v['locales'];
332
                    }
333
                }
334
335 2
                return $v;
336 90
            });
337 90
    }
338
339
    /**
340
     * Validate database options.
341
     *
342
     * @param NodeDefinition $root_node
343
     */
344 90
    private function validateDatabases(NodeDefinition $root_node): void
345
    {
346
        $root_node
347 90
            ->validate()
348
            ->ifTrue(static function ($v): bool {
349 70
                return is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
350 90
            })
351
            ->then(static function (array $v): array {
352 70
                foreach ($v['databases'] as $name => $database) {
353 56
                    if (empty($database['license'])) {
354 8
                        throw new \InvalidArgumentException(sprintf('License for downloaded database "%s" is not specified.', $name));
355
                    }
356
357 48
                    if (empty($database['edition'])) {
358 4
                        throw new \InvalidArgumentException(sprintf('Edition of downloaded database "%s" is not selected.', $name));
359
                    }
360
361 44
                    if (empty($database['url'])) {
362 4
                        throw new \InvalidArgumentException(sprintf('URL for download database "%s" is not specified.', $name));
363
                    }
364
365 40
                    if (empty($database['path'])) {
366 4
                        throw new \InvalidArgumentException(sprintf('The destination path to download database "%s" is not specified.', $name));
367
                    }
368
369 36
                    if (empty($database['locales'])) {
370 36
                        throw new \InvalidArgumentException(sprintf('The list of locales for database "%s" should not be empty.', $name));
371
                    }
372
                }
373
374 50
                return $v;
375 90
            });
376 90
    }
377
378
    /**
379
     * Normalize url option from license key and edition id.
380
     *
381
     * @param NodeDefinition $database_node
382
     */
383 90
    private function normalizeUrl(NodeDefinition $database_node): void
384
    {
385
        $database_node
386 90
            ->beforeNormalization()
387
            ->ifTrue(static function ($v): bool {
388
                return
389 74
                    is_array($v) &&
390 74
                    !array_key_exists('url', $v) &&
391 74
                    array_key_exists('license', $v) &&
392 74
                    array_key_exists('edition', $v);
393 90
            })
394
            ->then(static function (array $v): array {
395 42
                $v['url'] = sprintf(self::URL, urlencode($v['edition']), urlencode($v['license']));
396
397 42
                return $v;
398 90
            });
399 90
    }
400
401
    /**
402
     * Normalize path option from edition id.
403
     *
404
     * @param NodeDefinition $database_node
405
     */
406 90
    private function normalizePath(NodeDefinition $database_node): void
407
    {
408
        $database_node
409 90
            ->beforeNormalization()
410
            ->ifTrue(static function ($v): bool {
411 74
                return is_array($v) && !array_key_exists('path', $v) && array_key_exists('edition', $v);
412 90
            })
413
            ->then(function (array $v): array {
414 42
                $v['path'] = sprintf(self::PATH, $this->cache_dir, $v['edition']);
415
416 42
                return $v;
417 90
            });
418 90
    }
419
420
    /**
421
     * The url option must be a valid URL.
422
     *
423
     * @param NodeDefinition $url
424
     */
425 90
    private function validateURL(NodeDefinition $url): void
426
    {
427
        $url
428 90
            ->validate()
429
            ->ifTrue(static function ($v): bool {
430 68
                return is_string($v) && $v && !filter_var($v, FILTER_VALIDATE_URL);
431 90
            })
432
            ->then(static function (string $v): array {
433 2
                throw new \InvalidArgumentException(sprintf('URL "%s" must be valid.', $v));
434 90
            });
435 90
    }
436
}
437