Configuration::getConfigTreeBuilder()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 28
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 1

Importance

Changes 9
Bugs 0 Features 0
Metric Value
cc 1
eloc 20
nc 1
nop 0
dl 0
loc 28
ccs 21
cts 21
cp 1
crap 1
rs 9.6
c 9
b 0
f 0
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 96
    public function __construct(?string $cache_dir)
47
    {
48 96
        $this->cache_dir = $cache_dir ?: sys_get_temp_dir();
49 96
    }
50
51
    /**
52
     * @return TreeBuilder
53
     */
54 96
    public function getConfigTreeBuilder(): TreeBuilder
55
    {
56 96
        $tree_builder = $this->createTreeBuilder('gpslab_geoip');
57 96
        $root_node = $this->getRootNode($tree_builder, 'gpslab_geoip');
58
59 96
        $this->normalizeDefaultDatabase($root_node);
60 96
        $this->normalizeRootConfigurationToDefaultDatabase($root_node);
61 96
        $this->normalizeLicenseDirtyHack($root_node);
62 96
        $this->validateAvailableDefaultDatabase($root_node);
63 96
        $this->allowGlobalLicense($root_node);
64 96
        $this->allowGlobalLocales($root_node);
65 96
        $this->validateDatabases($root_node);
66
67 96
        $root_node->fixXmlConfig('locale');
68 96
        $locales = $root_node->children()->arrayNode('locales');
69 96
        $locales->prototype('scalar');
70 96
        $locales->treatNullLike([]);
71 96
        $locales->defaultValue(['en']);
72
73 96
        $root_node->children()->scalarNode('license');
74
75 96
        $default_database = $root_node->children()->scalarNode('default_database');
76 96
        $default_database->defaultValue('default');
77
78 96
        $root_node->fixXmlConfig('database');
79 96
        $root_node->append($this->getDatabaseNode());
80
81 96
        return $tree_builder;
82
    }
83
84
    /**
85
     * @return ArrayNodeDefinition
86
     */
87 96
    private function getDatabaseNode(): ArrayNodeDefinition
88
    {
89 96
        $tree_builder = $this->createTreeBuilder('databases');
90 96
        $root_node = $this->getRootNode($tree_builder, 'databases');
91 96
        $root_node->useAttributeAsKey('name');
92
93 96
        $database_node = $this->arrayPrototype($root_node);
94
95 96
        $this->normalizeUrl($database_node);
96 96
        $this->normalizePath($database_node);
97
98 96
        $url = $database_node->children()->scalarNode('url');
99 96
        $url->isRequired();
100
101 96
        $this->validateURL($url);
102
103 96
        $path = $database_node->children()->scalarNode('path');
104 96
        $path->isRequired();
105
106 96
        $database_node->fixXmlConfig('locale');
107 96
        $locales = $database_node->children()->arrayNode('locales');
108 96
        $locales->prototype('scalar');
109 96
        $locales->treatNullLike([]);
110 96
        $locales->defaultValue(['en']);
111
112 96
        $database_node->children()->scalarNode('license');
113
114 96
        $database_node->children()->enumNode('edition')->values(self::DATABASE_EDITION_IDS);
115
116 96
        return $root_node;
117
    }
118
119
    /**
120
     * @param string $name
121
     *
122
     * @return TreeBuilder
123
     */
124 96
    private function createTreeBuilder(string $name): TreeBuilder
125
    {
126
        // Symfony 4.2 +
127 96
        if (method_exists(TreeBuilder::class, '__construct')) {
128
            return new TreeBuilder($name);
129
        }
130
131
        // Symfony 4.1 and below
132 96
        return new TreeBuilder();
133
    }
134
135
    /**
136
     * @param TreeBuilder $tree_builder
137
     * @param string      $name
138
     *
139
     * @return ArrayNodeDefinition
140
     */
141 96
    private function getRootNode(TreeBuilder $tree_builder, string $name): ArrayNodeDefinition
142
    {
143 96
        if (method_exists($tree_builder, 'getRootNode')) {
144
            // Symfony 4.2 +
145
            $root = $tree_builder->getRootNode();
146
        } else {
147
            // Symfony 4.1 and below
148 96
            $root = $tree_builder->root($name);
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 96
        return $root;
158
    }
159
160
    /**
161
     * @param ArrayNodeDefinition $root_node
162
     *
163
     * @return ArrayNodeDefinition
164
     */
165 96
    private function arrayPrototype(ArrayNodeDefinition $root_node): ArrayNodeDefinition
166
    {
167
        // Symfony 3.3 +
168 96
        if (method_exists($root_node, 'arrayPrototype')) {
169
            return $root_node->arrayPrototype();
170
        }
171
172
        // Symfony 3.2 and below
173 96
        $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 96
        return $node;
182
    }
183
184
    /**
185
     * Normalize default_database from databases.
186
     *
187
     * @param NodeDefinition $root_node
188
     */
189 96
    private function normalizeDefaultDatabase(NodeDefinition $root_node): void
190
    {
191
        $root_node
192 96
            ->beforeNormalization()
193
            ->ifTrue(static function ($v): bool {
194
                return
195 91
                    is_array($v)
196 91
                    && !array_key_exists('default_database', $v)
197 91
                    && !empty($v['databases'])
198 91
                    && is_array($v['databases']);
199 96
            })
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 96
            });
206 96
    }
207
208
    /**
209
     * Normalize databases root configuration to default_database.
210
     *
211
     * @param NodeDefinition $root_node
212
     */
213 96
    private function normalizeRootConfigurationToDefaultDatabase(NodeDefinition $root_node): void
214
    {
215
        $root_node
216 96
            ->beforeNormalization()
217
            ->ifTrue(static function ($v): bool {
218 91
                return $v && is_array($v) && !array_key_exists('databases', $v) && !array_key_exists('database', $v);
219 96
            })
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 96
            });
232 96
    }
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 96
    private function normalizeLicenseDirtyHack(NodeDefinition $root_node): void
242
    {
243
        $root_node
244 96
            ->beforeNormalization()
245
            ->ifTrue(static function ($v): bool {
246 91
                return $v && is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
247 96
            })
248
            ->then(static function (array $v): array {
249 85
                foreach ($v['databases'] as $name => $database) {
250 81
                    if (isset($database['license']) && $database['license'] === self::LICENSE_DIRTY_HACK) {
251 4
                        unset($v['databases'][$name]);
252 81
                        @trigger_error(sprintf('License for downloaded database "%s" is not specified.', $name), E_USER_WARNING);
253
                    }
254
                }
255
256 85
                return $v;
257 96
            });
258 96
    }
259
260
    /**
261
     * Validate that the default_database exists in the list of databases.
262
     *
263
     * @param NodeDefinition $root_node
264
     */
265 96
    private function validateAvailableDefaultDatabase(NodeDefinition $root_node): void
266
    {
267
        $root_node
268 96
            ->validate()
269
            ->ifTrue(static function ($v): bool {
270
                return
271 80
                    is_array($v)
272 80
                    && array_key_exists('default_database', $v)
273 80
                    && !empty($v['databases'])
274 80
                    && !array_key_exists($v['default_database'], $v['databases']);
275 96
            })
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 96
            });
281 96
    }
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 96
    private function allowGlobalLicense(NodeDefinition $root_node): void
290
    {
291
        $root_node
292 96
            ->beforeNormalization()
293
            ->ifTrue(static function ($v): bool {
294
                return
295 91
                    is_array($v)
296 91
                    && array_key_exists('license', $v)
297 91
                    && array_key_exists('databases', $v)
298 91
                    && is_array($v['databases']);
299 96
            })
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 96
            });
309 96
    }
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 96
    private function allowGlobalLocales(NodeDefinition $root_node): void
318
    {
319
        $root_node
320 96
            ->beforeNormalization()
321
            ->ifTrue(static function ($v): bool {
322
                return
323 91
                    is_array($v)
324 91
                    && array_key_exists('locales', $v)
325 91
                    && array_key_exists('databases', $v)
326 91
                    && is_array($v['databases']);
327 96
            })
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 96
            });
337 96
    }
338
339
    /**
340
     * Validate database options.
341
     *
342
     * @param NodeDefinition $root_node
343
     */
344 96
    private function validateDatabases(NodeDefinition $root_node): void
345
    {
346
        $root_node
347 96
            ->validate()
348
            ->ifTrue(static function ($v): bool {
349 76
                return is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
350 96
            })
351
            ->then(static function (array $v): array {
352 76
                foreach ($v['databases'] as $name => $database) {
353 59
                    if (empty($database['license'])) {
354 8
                        throw new \InvalidArgumentException(sprintf('License for downloaded database "%s" is not specified.', $name));
355
                    }
356
357 51
                    if (empty($database['edition'])) {
358 4
                        throw new \InvalidArgumentException(sprintf('Edition of downloaded database "%s" is not selected.', $name));
359
                    }
360
361 47
                    if (empty($database['url'])) {
362 4
                        throw new \InvalidArgumentException(sprintf('URL for download database "%s" is not specified.', $name));
363
                    }
364
365 43
                    if (empty($database['path'])) {
366 4
                        throw new \InvalidArgumentException(sprintf('The destination path to download database "%s" is not specified.', $name));
367
                    }
368
369 39
                    if (empty($database['locales'])) {
370 39
                        throw new \InvalidArgumentException(sprintf('The list of locales for database "%s" should not be empty.', $name));
371
                    }
372
                }
373
374 56
                return $v;
375 96
            });
376 96
    }
377
378
    /**
379
     * Normalize url option from license key and edition id.
380
     *
381
     * @param NodeDefinition $database_node
382
     */
383 96
    private function normalizeUrl(NodeDefinition $database_node): void
384
    {
385
        $database_node
386 96
            ->beforeNormalization()
387
            ->ifTrue(static function ($v): bool {
388
                return
389 77
                    is_array($v)
390 77
                    && !array_key_exists('url', $v)
391 77
                    && array_key_exists('license', $v)
392 77
                    && array_key_exists('edition', $v);
393 96
            })
394
            ->then(static function (array $v): array {
395 45
                $v['url'] = sprintf(self::URL, urlencode($v['edition']), urlencode($v['license']));
396
397 45
                return $v;
398 96
            });
399 96
    }
400
401
    /**
402
     * Normalize path option from edition id.
403
     *
404
     * @param NodeDefinition $database_node
405
     */
406 96
    private function normalizePath(NodeDefinition $database_node): void
407
    {
408
        $database_node
409 96
            ->beforeNormalization()
410
            ->ifTrue(static function ($v): bool {
411 77
                return is_array($v) && !array_key_exists('path', $v) && array_key_exists('edition', $v);
412 96
            })
413
            ->then(function (array $v): array {
414 45
                $v['path'] = sprintf(self::PATH, $this->cache_dir, $v['edition']);
415
416 45
                return $v;
417 96
            });
418 96
    }
419
420
    /**
421
     * The url option must be a valid URL.
422
     *
423
     * @param NodeDefinition $url
424
     */
425 96
    private function validateURL(NodeDefinition $url): void
426
    {
427
        $url
428 96
            ->validate()
429
            ->ifTrue(static function ($v): bool {
430 71
                return is_string($v) && $v && !filter_var($v, FILTER_VALIDATE_URL);
431 96
            })
432
            ->then(static function (string $v): array {
433 2
                throw new \InvalidArgumentException(sprintf('URL "%s" must be valid.', $v));
434 96
            });
435 96
    }
436
}
437