Completed
Push — master ( 967e2b...3c9ba8 )
by Peter
13s queued 11s
created

Configuration   F

Complexity

Total Complexity 63

Size/Duplication

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