Completed
Push — master ( 2f7cd4...01e6b4 )
by Peter
02:53 queued 12s
created

Configuration   B

Complexity

Total Complexity 51

Size/Duplication

Total Lines 362
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Test Coverage

Coverage 98.04%

Importance

Changes 0
Metric Value
wmc 51
lcom 1
cbo 5
dl 0
loc 362
ccs 150
cts 153
cp 0.9804
rs 7.92
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 2
A getConfigTreeBuilder() 0 28 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
A validateAvailableDefaultDatabase() 0 17 4
B allowGlobalLicense() 0 21 6
B allowGlobalLocales() 0 21 6
A validateDatabaseLocales() 0 17 5
A normalizeUrl() 0 17 4
A normalizePath() 0 13 3
A validateURL() 0 11 2

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