Completed
Push — master ( 5511a0...f9009c )
by Peter
13s queued 11s
created

Configuration::validateDatabases()   B

Complexity

Conditions 9
Paths 1

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 33
ccs 18
cts 18
cp 1
rs 8.0555
c 0
b 0
f 0
cc 9
nc 1
nop 1
crap 9
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 62
    public function __construct(?string $cache_dir)
34
    {
35 62
        $this->cache_dir = $cache_dir ?: sys_get_temp_dir();
36 62
    }
37
38
    /**
39
     * @return TreeBuilder
40
     */
41 62
    public function getConfigTreeBuilder(): TreeBuilder
42
    {
43 62
        $tree_builder = $this->createTreeBuilder('gpslab_geoip');
44 62
        $root_node = $this->getRootNode($tree_builder, 'gpslab_geoip');
45
46 62
        $this->normalizeDefaultDatabase($root_node);
47 62
        $this->normalizeRootConfigurationToDefaultDatabase($root_node);
48 62
        $this->validateAvailableDefaultDatabase($root_node);
49 62
        $this->allowGlobalLicense($root_node);
50 62
        $this->allowGlobalLocales($root_node);
51 62
        $this->validateDatabases($root_node);
52
53 62
        $root_node->fixXmlConfig('locale');
54 62
        $locales = $root_node->children()->arrayNode('locales');
55 62
        $locales->prototype('scalar');
56 62
        $locales->treatNullLike([]);
57 62
        $locales->defaultValue(['en']);
58
59 62
        $root_node->children()->scalarNode('license');
60
61 62
        $default_database = $root_node->children()->scalarNode('default_database');
62 62
        $default_database->defaultValue('default');
63
64 62
        $root_node->fixXmlConfig('database');
65 62
        $root_node->append($this->getDatabaseNode());
66
67 62
        return $tree_builder;
68
    }
69
70
    /**
71
     * @return ArrayNodeDefinition
72
     */
73 62
    private function getDatabaseNode(): ArrayNodeDefinition
74
    {
75 62
        $tree_builder = $this->createTreeBuilder('databases');
76 62
        $root_node = $this->getRootNode($tree_builder, 'databases');
77 62
        $root_node->useAttributeAsKey('name');
78
79 62
        $database_node = $this->arrayPrototype($root_node);
80
81 62
        $this->normalizeUrl($database_node);
82 62
        $this->normalizePath($database_node);
83
84 62
        $url = $database_node->children()->scalarNode('url');
85 62
        $url->isRequired();
86
87 62
        $this->validateURL($url);
88
89 62
        $path = $database_node->children()->scalarNode('path');
90 62
        $path->isRequired();
91
92 62
        $database_node->fixXmlConfig('locale');
93 62
        $locales = $database_node->children()->arrayNode('locales');
94 62
        $locales->prototype('scalar');
95 62
        $locales->treatNullLike([]);
96 62
        $locales->defaultValue(['en']);
97
98 62
        $database_node->children()->scalarNode('license');
99
100 62
        $database_node->children()->scalarNode('edition');
101
102 62
        return $root_node;
103
    }
104
105
    /**
106
     * @param string $name
107
     *
108
     * @return TreeBuilder
109
     */
110 62
    private function createTreeBuilder(string $name): TreeBuilder
111
    {
112
        // Symfony 4.2 +
113 62
        if (method_exists(TreeBuilder::class, '__construct')) {
114
            return new TreeBuilder($name);
115
        }
116
117
        // Symfony 4.1 and below
118 62
        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 62
    private function getRootNode(TreeBuilder $tree_builder, string $name): ArrayNodeDefinition
128
    {
129 62
        if (method_exists($tree_builder, 'getRootNode')) {
130
            // Symfony 4.2 +
131
            $root = $tree_builder->getRootNode();
132
        } else {
133
            // Symfony 4.1 and below
134 62
            $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 62
        return $root;
144
    }
145
146
    /**
147
     * @param ArrayNodeDefinition $root_node
148
     *
149
     * @return ArrayNodeDefinition
150
     */
151 62
    private function arrayPrototype(ArrayNodeDefinition $root_node): ArrayNodeDefinition
152
    {
153
        // Symfony 3.3 +
154 62
        if (method_exists($root_node, 'arrayPrototype')) {
155 62
            return $root_node->arrayPrototype();
156
        }
157
158
        // Symfony 3.2 and below
159
        $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
        return $node;
168
    }
169
170
    /**
171
     * Normalize default_database from databases.
172
     *
173
     * @param NodeDefinition $root_node
174
     */
175 62
    private function normalizeDefaultDatabase(NodeDefinition $root_node): void
176
    {
177
        $root_node
178 62
            ->beforeNormalization()
179
            ->ifTrue(static function ($v): bool {
180
                return
181 57
                    is_array($v) &&
182 57
                    !array_key_exists('default_database', $v) &&
183 57
                    !empty($v['databases']) &&
184 57
                    is_array($v['databases']);
185 62
            })
186
            ->then(static function (array $v): array {
187 20
                $keys = array_keys($v['databases']);
188 20
                $v['default_database'] = reset($keys);
189
190 20
                return $v;
191 62
            });
192 62
    }
193
194
    /**
195
     * Normalize databases root configuration to default_database.
196
     *
197
     * @param NodeDefinition $root_node
198
     */
199 62
    private function normalizeRootConfigurationToDefaultDatabase(NodeDefinition $root_node): void
200
    {
201
        $root_node
202 62
            ->beforeNormalization()
203
            ->ifTrue(static function ($v): bool {
204 57
                return $v && is_array($v) && !array_key_exists('databases', $v) && !array_key_exists('database', $v);
205 62
            })
206
            ->then(static function (array $v): array {
207 18
                $database = $v;
208 18
                unset($database['default_database']);
209 18
                $default_database = isset($v['default_database']) ? (string) $v['default_database'] : 'default';
210
211
                return [
212 18
                    'default_database' => $default_database,
213
                    'databases' => [
214 18
                        $default_database => $database,
215
                    ],
216
                ];
217 62
            });
218 62
    }
219
220
    /**
221
     * Validate that the default_database exists in the list of databases.
222
     *
223
     * @param NodeDefinition $root_node
224
     */
225 62
    private function validateAvailableDefaultDatabase(NodeDefinition $root_node): void
226
    {
227
        $root_node
228 62
            ->validate()
229
            ->ifTrue(static function ($v): bool {
230
                return
231 52
                    is_array($v) &&
232 52
                    array_key_exists('default_database', $v) &&
233 52
                    !empty($v['databases']) &&
234 52
                    !array_key_exists($v['default_database'], $v['databases']);
235 62
            })
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 62
            });
241 62
    }
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 62
    private function allowGlobalLicense(NodeDefinition $root_node): void
250
    {
251
        $root_node
252 62
            ->beforeNormalization()
253
            ->ifTrue(static function ($v): bool {
254
                return
255 57
                    is_array($v) &&
256 57
                    array_key_exists('license', $v) &&
257 57
                    array_key_exists('databases', $v) &&
258 57
                    is_array($v['databases']);
259 62
            })
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 62
            });
269 62
    }
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 62
    private function allowGlobalLocales(NodeDefinition $root_node): void
278
    {
279
        $root_node
280 62
            ->beforeNormalization()
281
            ->ifTrue(static function ($v): bool {
282
                return
283 57
                    is_array($v) &&
284 57
                    array_key_exists('locales', $v) &&
285 57
                    array_key_exists('databases', $v) &&
286 57
                    is_array($v['databases']);
287 62
            })
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 62
            });
297 62
    }
298
299
    /**
300
     * Validate database options.
301
     *
302
     * @param NodeDefinition $root_node
303
     */
304 62
    private function validateDatabases(NodeDefinition $root_node): void
305
    {
306
        $root_node
307 62
            ->validate()
308
            ->ifTrue(static function ($v): bool {
309 48
                return is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
310 62
            })
311
            ->then(static function (array $v): array {
312 48
                foreach ($v['databases'] as $name => $database) {
313 35
                    if (empty($database['license'])) {
314 8
                        throw new \InvalidArgumentException(sprintf('License for downloaded databases "%s" is not specified.', $name));
315
                    }
316
317 27
                    if (empty($database['edition'])) {
318 8
                        throw new \InvalidArgumentException(sprintf('Edition of downloaded databases "%s" is not selected.', $name));
319
                    }
320
321 19
                    if (empty($database['url'])) {
322 4
                        throw new \InvalidArgumentException(sprintf('URL for download databases "%s" is not specified.', $name));
323
                    }
324
325 15
                    if (empty($database['path'])) {
326 4
                        throw new \InvalidArgumentException(sprintf('The destination path to download database "%s" is not specified.', $name));
327
                    }
328
329 11
                    if (empty($database['locales'])) {
330 11
                        throw new \InvalidArgumentException(sprintf('The list of locales for databases "%s" should not be empty.', $name));
331
                    }
332
                }
333
334 24
                return $v;
335 62
            });
336 62
    }
337
338
    /**
339
     * Normalize url option from license key and edition id.
340
     *
341
     * @param NodeDefinition $database_node
342
     */
343 62
    private function normalizeUrl(NodeDefinition $database_node): void
344
    {
345
        $database_node
346 62
            ->beforeNormalization()
347
            ->ifTrue(static function ($v): bool {
348
                return
349 47
                    is_array($v) &&
350 47
                    !array_key_exists('url', $v) &&
351 47
                    array_key_exists('license', $v) &&
352 47
                    array_key_exists('edition', $v);
353 62
            })
354
            ->then(static function (array $v): array {
355 15
                $v['url'] = sprintf(self::URL, urlencode($v['edition']), urlencode($v['license']));
356
357 15
                return $v;
358 62
            });
359 62
    }
360
361
    /**
362
     * Normalize path option from edition id.
363
     *
364
     * @param NodeDefinition $database_node
365
     */
366 62
    private function normalizePath(NodeDefinition $database_node): void
367
    {
368
        $database_node
369 62
            ->beforeNormalization()
370
            ->ifTrue(static function ($v): bool {
371 47
                return is_array($v) && !array_key_exists('path', $v) && array_key_exists('edition', $v);
372 62
            })
373
            ->then(function (array $v): array {
374 15
                $v['path'] = sprintf(self::PATH, $this->cache_dir, $v['edition']);
375
376 15
                return $v;
377 62
            });
378 62
    }
379
380
    /**
381
     * The url option must be a valid URL.
382
     *
383
     * @param NodeDefinition $url
384
     */
385 62
    private function validateURL(NodeDefinition $url): void
386
    {
387
        $url
388 62
            ->validate()
389
            ->ifTrue(static function ($v): bool {
390 41
                return is_string($v) && $v && !filter_var($v, FILTER_VALIDATE_URL);
391 62
            })
392
            ->then(static function (string $v): array {
393 2
                throw new \InvalidArgumentException(sprintf('URL "%s" must be valid.', $v));
394 62
            });
395 62
    }
396
}
397