Completed
Pull Request — master (#47)
by Peter
08:54
created

Configuration::normalizeDefaultDatabase()   A

Complexity

Conditions 5
Paths 1

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 5

Importance

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