Completed
Push — master ( b11ea7...6f99de )
by Peter
14s queued 12s
created

Configuration::getRootNode()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3.072

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 4
cts 5
cp 0.8
rs 9.6666
c 0
b 0
f 0
cc 3
nc 4
nop 2
crap 3.072
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
                    array_key_exists('databases', $v) &&
184 39
                    is_array($v['databases']) &&
185 39
                    $v['databases'];
186 41
            })
187
            ->then(static function (array $v): array {
188 10
                $keys = array_keys($v['databases']);
189 10
                $v['default_database'] = reset($keys);
190
191 10
                return $v;
192 41
            });
193 41
    }
194
195
    /**
196
     * Normalize databases root configuration to default_database.
197
     *
198
     * @param NodeDefinition $root_node
199
     */
200 41
    private function normalizeRootConfigurationToDefaultDatabase(NodeDefinition $root_node): void
201
    {
202
        $root_node
203 41
            ->beforeNormalization()
204
            ->ifTrue(static function ($v): bool {
205 39
                return $v && is_array($v) && !array_key_exists('databases', $v) && !array_key_exists('database', $v);
206 41
            })
207
            ->then(static function (array $v): array {
208
                // key that should not be rewritten to the database config
209 10
                $database = [];
210 10
                foreach ($v as $key => $value) {
211 10
                    if ($key !== 'default_database') {
212 10
                        $database[$key] = $v[$key];
213 10
                        unset($v[$key]);
214
                    }
215
                }
216 10
                $v['default_database'] = isset($v['default_database']) ? (string) $v['default_database'] : 'default';
217 10
                $v['databases'] = [$v['default_database'] => $database];
218
219 10
                return $v;
220 41
            });
221 41
    }
222
223
    /**
224
     * Validate that the default_database exists in the list of databases.
225
     *
226
     * @param NodeDefinition $root_node
227
     */
228 41
    private function validateAvailableDefaultDatabase(NodeDefinition $root_node): void
229
    {
230
        $root_node
231 41
            ->validate()
232
                ->ifTrue(static function ($v): bool {
233
                    return
234 35
                        is_array($v) &&
235 35
                        array_key_exists('default_database', $v) &&
236 35
                        array_key_exists('databases', $v) &&
237 35
                        $v['databases'] &&
238 35
                        !array_key_exists($v['default_database'], $v['databases']);
239 41
                })
240
                ->then(static function (array $v): array {
241 4
                    $databases = implode('", "', array_keys($v['databases']));
242
243 4
                    throw new \InvalidArgumentException(sprintf('Undefined default database "%s". Available "%s" databases.', $v['default_database'], $databases));
244 41
                });
245 41
    }
246
247
    /**
248
     * Add a license option to the databases configuration if it does not exist.
249
     * Allow use a global license for all databases.
250
     *
251
     * @param NodeDefinition $root_node
252
     */
253 41
    private function allowGlobalLicense(NodeDefinition $root_node): void
254
    {
255
        $root_node
256 41
            ->beforeNormalization()
257
            ->ifTrue(static function ($v): bool {
258
                return
259 39
                    is_array($v) &&
260 39
                    array_key_exists('license', $v) &&
261 39
                    array_key_exists('databases', $v) &&
262 39
                    is_array($v['databases']);
263 41
            })
264
            ->then(static function (array $v): array {
265 6
                foreach ($v['databases'] as $name => $database) {
266 4
                    if (!array_key_exists('license', $database)) {
267 4
                        $v['databases'][$name]['license'] = $v['license'];
268
                    }
269
                }
270
271 6
                return $v;
272 41
            });
273 41
    }
274
275
    /**
276
     * Add a locales option to the databases configuration if it does not exist.
277
     * Allow use a global locales for all databases.
278
     *
279
     * @param NodeDefinition $root_node
280
     */
281 41
    private function allowGlobalLocales(NodeDefinition $root_node): void
282
    {
283
        $root_node
284 41
            ->beforeNormalization()
285
            ->ifTrue(static function ($v): bool {
286
                return
287 39
                    is_array($v) &&
288 39
                    array_key_exists('locales', $v) &&
289 39
                    array_key_exists('databases', $v) &&
290 39
                    is_array($v['databases']);
291 41
            })
292
            ->then(static function (array $v): array {
293 2
                foreach ($v['databases'] as $name => $database) {
294 2
                    if (!array_key_exists('locales', $database)) {
295 2
                        $v['databases'][$name]['locales'] = $v['locales'];
296
                    }
297
                }
298
299 2
                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 31
                return is_array($v) && array_key_exists('databases', $v) && is_array($v['databases']);
314 41
            })
315
            ->then(static function (array $v): array {
316 31
                foreach ($v['databases'] as $name => $database) {
317 21
                    if (empty($database['locales'])) {
318 21
                        throw new \InvalidArgumentException(sprintf('The list of locales should not be empty in databases "%s".', $name));
319
                    }
320
                }
321
322 27
                return $v;
323 41
            });
324 41
    }
325
326
    /**
327
     * Normalize url option from license key and edition id.
328
     *
329
     * @param NodeDefinition $database_node
330
     */
331 41
    private function normalizeUrl(NodeDefinition $database_node): void
332
    {
333
        $database_node
334 41
            ->beforeNormalization()
335
            ->ifTrue(static function ($v): bool {
336
                return
337 29
                    is_array($v) &&
338 29
                    !array_key_exists('url', $v) &&
339 29
                    array_key_exists('license', $v) &&
340 29
                    array_key_exists('edition', $v);
341 41
            })
342
            ->then(static function (array $v): array {
343 19
                $v['url'] = sprintf(self::URL, urlencode($v['edition']), urlencode($v['license']));
344
345 19
                return $v;
346 41
            });
347 41
    }
348
349
    /**
350
     * Normalize path option from edition id.
351
     *
352
     * @param NodeDefinition $database_node
353
     */
354 41
    private function normalizePath(NodeDefinition $database_node): void
355
    {
356
        $database_node
357 41
            ->beforeNormalization()
358
            ->ifTrue(static function ($v): bool {
359 29
                return is_array($v) && !array_key_exists('path', $v) && array_key_exists('edition', $v);
360 41
            })
361
            ->then(function (array $v): array {
362 23
                $v['path'] = sprintf(self::PATH, $this->cache_dir, $v['edition']);
363
364 23
                return $v;
365 41
            });
366 41
    }
367
368
    /**
369
     * The url option must be a valid URL.
370
     *
371
     * @param NodeDefinition $url
372
     */
373 41
    private function validateURL(NodeDefinition $url): void
374
    {
375
        $url
376 41
            ->validate()
377
            ->ifTrue(static function ($v): bool {
378 27
                return is_string($v) && !filter_var($v, FILTER_VALIDATE_URL);
379 41
            })
380
            ->then(static function (string $v): array {
381 2
                throw new \InvalidArgumentException(sprintf('URL "%s" must be valid.', $v));
382 41
            });
383 41
    }
384
}
385