Passed
Push — upstream-8.9.2 ( 6a22e8 )
by Joshua
25:34 queued 09:29
created

BuildMetadataFromXml   F

Complexity

Total Complexity 107

Size/Duplication

Total Lines 782
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 782
rs 1.263
c 0
b 0
f 0
wmc 107

20 Methods

Rating   Name   Duplication   Size   Complexity  
B setRelevantDescPatterns() 0 33 2
A setLeadingDigitsPatterns() 0 7 3
A getNationalPrefixFormattingRuleFromElement() 0 7 1
C processPhoneNumberDescElement() 0 54 10
A getMetadataFilter() 0 13 4
C loadAvailableFormats() 0 53 10
A loadNationalFormat() 0 15 3
C parsePossibleLengthStringToSet() 0 51 12
A getDomesticCarrierCodeFormattingRuleFromElement() 0 7 1
B buildPhoneMetadataCollection() 0 32 4
A getNationalPrefix() 0 3 2
B populatePossibleLengthSets() 0 31 4
C setPossibleLengths() 0 38 11
C setPossibleLengthsGeneralDesc() 0 47 9
A validateRE() 0 15 4
B buildCountryCodeToRegionCodeMap() 0 24 5
A loadCountryMetadata() 0 12 2
F loadTerritoryTagMetadata() 0 40 10
A arePossibleLengthsEqual() 0 16 4
B loadInternationalFormat() 0 29 6

How to fix   Complexity   

Complex Class

Complex classes like BuildMetadataFromXml 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.

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 BuildMetadataFromXml, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace libphonenumber\buildtools;
4
5
use libphonenumber\NumberFormat;
6
use libphonenumber\PhoneMetadata;
7
use libphonenumber\PhoneNumberDesc;
8
9
/**
10
 * Library to build phone number metadata from the XML format.
11
 *
12
 * @author Davide Mendolia
13
 * @internal
14
 */
15
class BuildMetadataFromXml
16
{
17
    // String constants used to fetch the XML nodes and attributes.
18
    const CARRIER_CODE_FORMATTING_RULE = "carrierCodeFormattingRule";
19
    const COUNTRY_CODE = "countryCode";
20
    const EMERGENCY = "emergency";
21
    const EXAMPLE_NUMBER = "exampleNumber";
22
    const FIXED_LINE = "fixedLine";
23
    const FORMAT = "format";
24
    const GENERAL_DESC = "generalDesc";
25
    const INTERNATIONAL_PREFIX = "internationalPrefix";
26
    const INTL_FORMAT = "intlFormat";
27
    const LEADING_DIGITS = "leadingDigits";
28
    const MOBILE_NUMBER_PORTABLE_REGION = "mobileNumberPortableRegion";
29
    const MAIN_COUNTRY_FOR_CODE = "mainCountryForCode";
30
    const MOBILE = "mobile";
31
    const NATIONAL_NUMBER_PATTERN = "nationalNumberPattern";
32
    const NATIONAL_PREFIX = "nationalPrefix";
33
    const NATIONAL_PREFIX_FORMATTING_RULE = "nationalPrefixFormattingRule";
34
    const NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING = "nationalPrefixOptionalWhenFormatting";
35
    const NATIONAL_PREFIX_FOR_PARSING = "nationalPrefixForParsing";
36
    const NATIONAL_PREFIX_TRANSFORM_RULE = "nationalPrefixTransformRule";
37
    const NO_INTERNATIONAL_DIALLING = "noInternationalDialling";
38
    const NUMBER_FORMAT = "numberFormat";
39
    const PAGER = "pager";
40
    const CARRIER_SPECIFIC = 'carrierSpecific';
41
    const PATTERN = "pattern";
42
    const PERSONAL_NUMBER = "personalNumber";
43
    const POSSIBLE_LENGTHS = "possibleLengths";
44
    const NATIONAL = "national";
45
    const LOCAL_ONLY = "localOnly";
46
    const PREFERRED_EXTN_PREFIX = "preferredExtnPrefix";
47
    const PREFERRED_INTERNATIONAL_PREFIX = "preferredInternationalPrefix";
48
    const PREMIUM_RATE = "premiumRate";
49
    const SHARED_COST = "sharedCost";
50
    const SHORT_CODE = "shortCode";
51
    const SMS_SERVICES = "smsServices";
52
    const STANDARD_RATE = "standardRate";
53
    const TOLL_FREE = "tollFree";
54
    const UAN = "uan";
55
    const VOICEMAIL = "voicemail";
56
    const VOIP = "voip";
57
58
    private static $phoneNumberDescsWithoutMatchingTypes = array(
59
        self::NO_INTERNATIONAL_DIALLING
60
    );
61
62
    /**
63
     * @internal
64
     * @param string $regex
65
     * @param bool $removeWhitespace
66
     * @return string
67
     */
68
    public static function validateRE($regex, $removeWhitespace = false)
69
    {
70
        $compressedRegex = $removeWhitespace ? preg_replace('/\\s/', '', $regex) : $regex;
71
        // Match regex against an empty string to check the regex is valid
72
        if (preg_match('/' . $compressedRegex . '/', '') === false) {
73
            throw new \RuntimeException("Regex error: " . preg_last_error());
74
        }
75
        // We don't ever expect to see | followed by a ) in our metadata - this would be an indication
76
        // of a bug. If one wants to make something optional, we prefer ? to using an empty group.
77
        $errorIndex = strpos($compressedRegex, '|)');
78
        if ($errorIndex !== false) {
79
            throw new \RuntimeException("| followed by )");
80
        }
81
        // return the regex if it is of correct syntax, i.e. compile did not fail with a
82
        return $compressedRegex;
83
    }
84
85
    /**
86
     *
87
     * @param string $inputXmlFile
88
     * @param boolean $liteBuild
89
     * @param boolean $specialBuild
90
     * @param bool $isShortNumberMetadata
91
     * @param bool $isAlternateFormatsMetadata
92
     * @return PhoneMetadata[]
93
     */
94
    public static function buildPhoneMetadataCollection($inputXmlFile, $liteBuild, $specialBuild, $isShortNumberMetadata = false, $isAlternateFormatsMetadata = false)
95
    {
96
        if ($inputXmlFile instanceof \DOMElement) {
0 ignored issues
show
introduced by
$inputXmlFile is never a sub-type of DOMElement.
Loading history...
97
            $document = $inputXmlFile;
98
        } else {
99
            $document = new \DOMDocument();
100
            $document->load($inputXmlFile);
101
            $document->normalizeDocument();
102
103
            $isShortNumberMetadata = strpos($inputXmlFile, 'ShortNumberMetadata');
104
            $isAlternateFormatsMetadata = strpos($inputXmlFile, 'PhoneNumberAlternateFormats');
105
        }
106
107
        $territories = $document->getElementsByTagName("territory");
108
        $metadataCollection = array();
109
110
        $metadataFilter = self::getMetadataFilter($liteBuild, $specialBuild);
111
112
        foreach ($territories as $territoryElement) {
113
            /** @var $territoryElement \DOMElement */
114
            // For the main metadata file this should always be set, but for other supplementary data
115
            // files the country calling code may be all that is needed.
116
            if ($territoryElement->hasAttribute("id")) {
117
                $regionCode = $territoryElement->getAttribute("id");
118
            } else {
119
                $regionCode = "";
120
            }
121
            $metadata = self::loadCountryMetadata($regionCode, $territoryElement, $isShortNumberMetadata, $isAlternateFormatsMetadata);
122
            $metadataFilter->filterMetadata($metadata);
123
            $metadataCollection[] = $metadata;
124
        }
125
        return $metadataCollection;
126
    }
127
128
    /**
129
     * @param string $regionCode
130
     * @param \DOMElement $element
131
     * @param string $isShortNumberMetadata
132
     * @param string $isAlternateFormatsMetadata
133
     * @return PhoneMetadata
134
     */
135
    public static function loadCountryMetadata($regionCode, \DOMElement $element, $isShortNumberMetadata, $isAlternateFormatsMetadata)
136
    {
137
        $nationalPrefix = self::getNationalPrefix($element);
138
        $metadata = self::loadTerritoryTagMetadata($regionCode, $element, $nationalPrefix);
139
        $nationalPrefixFormattingRule = self::getNationalPrefixFormattingRuleFromElement($element, $nationalPrefix);
140
141
        self::loadAvailableFormats($metadata, $element, $nationalPrefix, $nationalPrefixFormattingRule, $element->hasAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING));
142
        if (!$isAlternateFormatsMetadata) {
143
            // The alternate formats metadata does not need most of the patterns to be set.
144
            self::setRelevantDescPatterns($metadata, $element, $isShortNumberMetadata);
0 ignored issues
show
Bug introduced by
$isShortNumberMetadata of type string is incompatible with the type boolean expected by parameter $isShortNumberMetadata of libphonenumber\buildtool...tRelevantDescPatterns(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

144
            self::setRelevantDescPatterns($metadata, $element, /** @scrutinizer ignore-type */ $isShortNumberMetadata);
Loading history...
145
        }
146
        return $metadata;
147
    }
148
149
    /**
150
     * Processes the custom build flags and gets a MetadataFilter which may be used to
151
     * filter PhoneMetadata objects. Incompatible flag combinations throw RuntimeException.
152
     * @param bool $liteBuild
153
     * @param bool $specialBuild
154
     * @return MetadataFilter
155
     */
156
    public static function getMetadataFilter($liteBuild, $specialBuild)
157
    {
158
        if ($specialBuild) {
159
            if ($liteBuild) {
160
                throw new \RuntimeException("liteBuild and specialBuild may not both be set");
161
            }
162
            return MetadataFilter::forSpecialBuild();
163
        }
164
        if ($liteBuild) {
165
            return MetadataFilter::forLiteBuild();
166
        }
167
168
        return MetadataFilter::emptyFilter();
169
    }
170
171
    /**
172
     * Returns the national prefix of the provided country element.
173
     * @internal
174
     * @param \DOMElement $element
175
     * @return string
176
     */
177
    public static function getNationalPrefix(\DOMElement $element)
178
    {
179
        return $element->hasAttribute(self::NATIONAL_PREFIX) ? $element->getAttribute(self::NATIONAL_PREFIX) : "";
180
    }
181
182
    /**
183
     *
184
     * @internal
185
     * @param \DOMElement $element
186
     * @param string $nationalPrefix
187
     * @return string
188
     */
189
    public static function getNationalPrefixFormattingRuleFromElement(\DOMElement $element, $nationalPrefix)
190
    {
191
        $nationalPrefixFormattingRule = $element->getAttribute(self::NATIONAL_PREFIX_FORMATTING_RULE);
192
        // Replace $NP with national prefix and $FG with the first group ($1).
193
        $nationalPrefixFormattingRule = str_replace('$NP', $nationalPrefix, $nationalPrefixFormattingRule);
194
        $nationalPrefixFormattingRule = str_replace('$FG', '$1', $nationalPrefixFormattingRule);
195
        return $nationalPrefixFormattingRule;
196
    }
197
198
    /**
199
     *
200
     * @internal
201
     * @param string $regionCode
202
     * @param \DOMElement $element
203
     * @param string $nationalPrefix
204
     * @return PhoneMetadata
205
     */
206
    public static function loadTerritoryTagMetadata(
207
        $regionCode,
208
        \DOMElement $element,
209
        $nationalPrefix
210
    ) {
211
        $metadata = new PhoneMetadata();
212
        $metadata->setId($regionCode);
213
        $metadata->setCountryCode((int)$element->getAttribute(self::COUNTRY_CODE));
214
        if ($element->hasAttribute(self::LEADING_DIGITS)) {
215
            $metadata->setLeadingDigits(self::validateRE($element->getAttribute(self::LEADING_DIGITS)));
216
        }
217
        $metadata->setInternationalPrefix(self::validateRE($element->getAttribute(self::INTERNATIONAL_PREFIX)));
218
        if ($element->hasAttribute(self::PREFERRED_INTERNATIONAL_PREFIX)) {
219
            $preferredInternationalPrefix = $element->getAttribute(self::PREFERRED_INTERNATIONAL_PREFIX);
220
            $metadata->setPreferredInternationalPrefix($preferredInternationalPrefix);
221
        }
222
        if ($element->hasAttribute(self::NATIONAL_PREFIX_FOR_PARSING)) {
223
            $metadata->setNationalPrefixForParsing(
224
                self::validateRE($element->getAttribute(self::NATIONAL_PREFIX_FOR_PARSING), true)
225
            );
226
            if ($element->hasAttribute(self::NATIONAL_PREFIX_TRANSFORM_RULE)) {
227
                $metadata->setNationalPrefixTransformRule(self::validateRE($element->getAttribute(self::NATIONAL_PREFIX_TRANSFORM_RULE)));
228
            }
229
        }
230
        if ($nationalPrefix != '') {
231
            $metadata->setNationalPrefix($nationalPrefix);
232
            if (!$metadata->hasNationalPrefixForParsing()) {
233
                $metadata->setNationalPrefixForParsing($nationalPrefix);
234
            }
235
        }
236
        if ($element->hasAttribute(self::PREFERRED_EXTN_PREFIX)) {
237
            $metadata->setPreferredExtnPrefix($element->getAttribute(self::PREFERRED_EXTN_PREFIX));
238
        }
239
        if ($element->hasAttribute(self::MAIN_COUNTRY_FOR_CODE)) {
240
            $metadata->setMainCountryForCode(true);
241
        }
242
        if ($element->hasAttribute(self::MOBILE_NUMBER_PORTABLE_REGION)) {
243
            $metadata->setMobileNumberPortableRegion(true);
244
        }
245
        return $metadata;
246
    }
247
248
    /**
249
     * Extracts the available formats from the provided DOM element. If it does not contain any
250
     * nationalPrefixFormattingRule, the one passed-in is retained; similarly for
251
     * nationalPrefixOptionalWhenFormatting. The nationalPrefix, nationalPrefixFormattingRule and
252
     * nationalPrefixOptionalWhenFormatting values are provided from the parent (territory) element.
253
     * @internal
254
     * @param PhoneMetadata $metadata
255
     * @param \DOMElement $element
256
     * @param string $nationalPrefix
257
     * @param string $nationalPrefixFormattingRule
258
     * @param bool $nationalPrefixOptionalWhenFormatting
259
     */
260
    public static function loadAvailableFormats(
261
        PhoneMetadata $metadata,
262
        \DOMElement $element,
263
        $nationalPrefix,
264
        $nationalPrefixFormattingRule,
265
        $nationalPrefixOptionalWhenFormatting
266
    ) {
267
        $carrierCodeFormattingRule = "";
268
        if ($element->hasAttribute(self::CARRIER_CODE_FORMATTING_RULE)) {
269
            $carrierCodeFormattingRule = self::validateRE(self::getDomesticCarrierCodeFormattingRuleFromElement($element, $nationalPrefix));
270
        }
271
        $numberFormatElements = $element->getElementsByTagName(self::NUMBER_FORMAT);
272
        $hasExplicitIntlFormatDefined = false;
273
274
        $numOfFormatElements = $numberFormatElements->length;
275
        if ($numOfFormatElements > 0) {
276
            for ($i = 0; $i < $numOfFormatElements; $i++) {
277
                /** @var \DOMElement $numberFormatElement */
278
                $numberFormatElement = $numberFormatElements->item($i);
279
                $format = new NumberFormat();
280
281
                if ($numberFormatElement->hasAttribute(self::NATIONAL_PREFIX_FORMATTING_RULE)) {
282
                    $format->setNationalPrefixFormattingRule(
283
                        self::getNationalPrefixFormattingRuleFromElement($numberFormatElement, $nationalPrefix)
284
                    );
285
                } else {
286
                    $format->setNationalPrefixFormattingRule($nationalPrefixFormattingRule);
287
                }
288
                if ($numberFormatElement->hasAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)) {
289
                    $format->setNationalPrefixOptionalWhenFormatting($numberFormatElement->getAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING) == 'true' ? true : false);
290
                } else {
291
                    $format->setNationalPrefixOptionalWhenFormatting($nationalPrefixOptionalWhenFormatting);
292
                }
293
                if ($numberFormatElement->hasAttribute(self::CARRIER_CODE_FORMATTING_RULE)) {
294
                    $format->setDomesticCarrierCodeFormattingRule(
295
                        self::validateRE(self::getDomesticCarrierCodeFormattingRuleFromElement($numberFormatElement, $nationalPrefix))
296
                    );
297
                } else {
298
                    $format->setDomesticCarrierCodeFormattingRule($carrierCodeFormattingRule);
299
                }
300
                self::loadNationalFormat($metadata, $numberFormatElement, $format);
301
                $metadata->addNumberFormat($format);
302
303
                if (self::loadInternationalFormat($metadata, $numberFormatElement, $format)) {
304
                    $hasExplicitIntlFormatDefined = true;
305
                }
306
            }
307
            // Only a small number of regions need to specify the intlFormats in the xml. For the majority
308
            // of countries the intlNumberFormat metadata is an exact copy of the national NumberFormat
309
            // metadata. To minimize the size of the metadata file, we only keep intlNumberFormats that
310
            // actually differ in some way to the national formats.
311
            if (!$hasExplicitIntlFormatDefined) {
312
                $metadata->clearIntlNumberFormat();
313
            }
314
        }
315
    }
316
317
    /**
318
     * @internal
319
     * @param \DOMElement $element
320
     * @param string $nationalPrefix
321
     * @return string
322
     */
323
    public static function getDomesticCarrierCodeFormattingRuleFromElement(\DOMElement $element, $nationalPrefix)
324
    {
325
        $carrierCodeFormattingRule = $element->getAttribute(self::CARRIER_CODE_FORMATTING_RULE);
326
        // Replace $FG with the first group ($1) and $NP with the national prefix.
327
        $carrierCodeFormattingRule = str_replace('$NP', $nationalPrefix, $carrierCodeFormattingRule);
328
        $carrierCodeFormattingRule = str_replace('$FG', '$1', $carrierCodeFormattingRule);
329
        return $carrierCodeFormattingRule;
330
    }
331
332
    /**
333
     * Extracts the pattern for the national format.
334
     *
335
     * @internal
336
     * @param PhoneMetadata $metadata
337
     * @param \DOMElement $numberFormatElement
338
     * @param NumberFormat $format
339
     * @throws \RuntimeException if multiple or no formats have been encountered.
340
     */
341
    public static function loadNationalFormat(
342
        PhoneMetadata $metadata,
343
        \DOMElement $numberFormatElement,
344
        NumberFormat $format
345
    ) {
346
        self::setLeadingDigitsPatterns($numberFormatElement, $format);
347
        $format->setPattern(self::validateRE($numberFormatElement->getAttribute(self::PATTERN)));
348
349
        $formatPattern = $numberFormatElement->getElementsByTagName(self::FORMAT);
350
        if ($formatPattern->length != 1) {
351
            $countryId = strlen($metadata->getId()) > 0 ? $metadata->getId() : $metadata->getCountryCode();
352
            throw new \RuntimeException("Invalid number of format patterns for country: " . $countryId);
353
        }
354
        $nationalFormat = $formatPattern->item(0)->firstChild->nodeValue;
355
        $format->setFormat($nationalFormat);
356
    }
357
358
    /**
359
     * @internal
360
     * @param \DOMElement $numberFormatElement
361
     * @param NumberFormat $format
362
     */
363
    public static function setLeadingDigitsPatterns(\DOMElement $numberFormatElement, NumberFormat $format)
364
    {
365
        $leadingDigitsPatternNodes = $numberFormatElement->getElementsByTagName(self::LEADING_DIGITS);
366
        $numOfLeadingDigitsPatterns = $leadingDigitsPatternNodes->length;
367
        if ($numOfLeadingDigitsPatterns > 0) {
368
            for ($i = 0; $i < $numOfLeadingDigitsPatterns; $i++) {
369
                $format->addLeadingDigitsPattern(self::validateRE($leadingDigitsPatternNodes->item($i)->firstChild->nodeValue, true));
370
            }
371
        }
372
    }
373
374
    /**
375
     * Extracts the pattern for international format. If there is no intlFormat, default to using the
376
     * national format. If the intlFormat is set to "NA" the intlFormat should be ignored.
377
     *
378
     * @internal
379
     * @param PhoneMetadata $metadata
380
     * @param \DOMElement $numberFormatElement
381
     * @param NumberFormat $nationalFormat
382
     * @throws \RuntimeException if multiple intlFormats have been encountered.
383
     * @return bool whether an international number format is defined.
384
     */
385
    public static function loadInternationalFormat(
386
        PhoneMetadata $metadata,
387
        \DOMElement $numberFormatElement,
388
        NumberFormat $nationalFormat
389
    ) {
390
        $intlFormat = new NumberFormat();
391
        $intlFormatPattern = $numberFormatElement->getElementsByTagName(self::INTL_FORMAT);
392
        $hasExplicitIntlFormatDefined = false;
393
394
        if ($intlFormatPattern->length > 1) {
395
            $countryId = strlen($metadata->getId()) > 0 ? $metadata->getId() : $metadata->getCountryCode();
396
            throw new \RuntimeException("Invalid number of intlFormat patterns for country: " . $countryId);
397
        } elseif ($intlFormatPattern->length == 0) {
398
            // Default to use the same as the national pattern if none is defined.
399
            $intlFormat->mergeFrom($nationalFormat);
400
        } else {
401
            $intlFormat->setPattern($numberFormatElement->getAttribute(self::PATTERN));
402
            self::setLeadingDigitsPatterns($numberFormatElement, $intlFormat);
403
            $intlFormatPatternValue = $intlFormatPattern->item(0)->firstChild->nodeValue;
404
            if ($intlFormatPatternValue !== "NA") {
405
                $intlFormat->setFormat($intlFormatPatternValue);
406
            }
407
            $hasExplicitIntlFormatDefined = true;
408
        }
409
410
        if ($intlFormat->hasFormat()) {
411
            $metadata->addIntlNumberFormat($intlFormat);
412
        }
413
        return $hasExplicitIntlFormatDefined;
414
    }
415
416
    /**
417
     * @internal
418
     * @param PhoneMetadata $metadata
419
     * @param \DOMElement $element
420
     * @param bool $isShortNumberMetadata
421
     */
422
    public static function setRelevantDescPatterns(PhoneMetadata $metadata, \DOMElement $element, $isShortNumberMetadata)
423
    {
424
        $generalDesc = self::processPhoneNumberDescElement(null, $element, self::GENERAL_DESC);
425
        $metadata->setGeneralDesc($generalDesc);
426
427
        $metadataId = $metadata->getId();
428
        // Calculate the possible lengths for the general description. This will be based on the
429
        // possible lengths of the child elements.
430
        self::setPossibleLengthsGeneralDesc($generalDesc, $metadataId, $element, $isShortNumberMetadata);
431
432
        if (!$isShortNumberMetadata) {
433
            // Set fields used by regular length phone numbers.
434
            $metadata->setFixedLine(self::processPhoneNumberDescElement($generalDesc, $element, self::FIXED_LINE));
435
            $metadata->setMobile(self::processPhoneNumberDescElement($generalDesc, $element, self::MOBILE));
436
            $metadata->setSharedCost(self::processPhoneNumberDescElement($generalDesc, $element, self::SHARED_COST));
437
            $metadata->setVoip(self::processPhoneNumberDescElement($generalDesc, $element, self::VOIP));
438
            $metadata->setPersonalNumber(self::processPhoneNumberDescElement($generalDesc, $element, self::PERSONAL_NUMBER));
439
            $metadata->setPager(self::processPhoneNumberDescElement($generalDesc, $element, self::PAGER));
440
            $metadata->setUan(self::processPhoneNumberDescElement($generalDesc, $element, self::UAN));
441
            $metadata->setVoicemail(self::processPhoneNumberDescElement($generalDesc, $element, self::VOICEMAIL));
442
            $metadata->setNoInternationalDialling(self::processPhoneNumberDescElement($generalDesc, $element, self::NO_INTERNATIONAL_DIALLING));
443
            $metadata->setSameMobileAndFixedLinePattern($metadata->getMobile()->getNationalNumberPattern() === $metadata->getFixedLine()->getNationalNumberPattern());
444
            $metadata->setTollFree(self::processPhoneNumberDescElement($generalDesc, $element, self::TOLL_FREE));
445
            $metadata->setPremiumRate(self::processPhoneNumberDescElement($generalDesc, $element, self::PREMIUM_RATE));
446
        } else {
447
            // Set fields used by short numbers.
448
            $metadata->setStandardRate(self::processPhoneNumberDescElement($generalDesc, $element, self::STANDARD_RATE));
449
            $metadata->setShortCode(self::processPhoneNumberDescElement($generalDesc, $element, self::SHORT_CODE));
450
            $metadata->setCarrierSpecific(self::processPhoneNumberDescElement($generalDesc, $element, self::CARRIER_SPECIFIC));
451
            $metadata->setEmergency(self::processPhoneNumberDescElement($generalDesc, $element, self::EMERGENCY));
452
            $metadata->setTollFree(self::processPhoneNumberDescElement($generalDesc, $element, self::TOLL_FREE));
453
            $metadata->setPremiumRate(self::processPhoneNumberDescElement($generalDesc, $element, self::PREMIUM_RATE));
454
            $metadata->setSmsServices(self::processPhoneNumberDescElement($generalDesc, $element, self::SMS_SERVICES));
455
        }
456
    }
457
458
    /**
459
     * Parses a possible length string into a set of the integers that are covered.
460
     *
461
     * @param string $possibleLengthString a string specifying the possible lengths of phone numbers. Follows
462
     * this syntax: ranges or elements are separated by commas, and ranges are specified in
463
     * [min-max] notation, inclusive. For example, [3-5],7,9,[11-14] should be parsed to
464
     * 3,4,5,7,9,11,12,13,14
465
     * @return array
466
     */
467
    private static function parsePossibleLengthStringToSet($possibleLengthString)
468
    {
469
        if (strlen($possibleLengthString) === 0) {
470
            throw new \RuntimeException("Empty possibleLength string found.");
471
        }
472
473
        $lengths = explode(",", $possibleLengthString);
474
        $lengthSet = array();
475
476
477
        $lengthLength = count($lengths);
478
        for ($i = 0; $i < $lengthLength; $i++) {
479
            $lengthSubstring = $lengths[$i];
480
            if (strlen($lengthSubstring) === 0) {
481
                throw new \RuntimeException("Leading, trailing or adjacent commas in possible "
482
                    . "length string {$possibleLengthString}, these should only separate numbers or ranges.");
483
            } elseif (substr($lengthSubstring, 0, 1) === '[') {
484
                if (substr($lengthSubstring, -1) !== ']') {
485
                    throw new \RuntimeException("Missing end of range character in possible length string {$possibleLengthString}.");
486
                }
487
488
                // Strip the leading and trailing [], and split on the -.
489
                $minMax = explode('-', substr($lengthSubstring, 1, -1));
490
                if (count($minMax) !== 2) {
491
                    throw new \RuntimeException("Ranges must have exactly one - character: missing for {$possibleLengthString}.");
492
                }
493
                $min = (int)$minMax[0];
494
                $max = (int)$minMax[1];
495
                // We don't even accept [6-7] since we prefer the shorter 6,7 variant; for a range to be in
496
                // use the hyphen needs to replace at least one digit.
497
                if ($max - $min < 2) {
498
                    throw new \RuntimeException("The first number in a range should be two or more digits lower than the second. Culprit possibleLength string: {$possibleLengthString}.");
499
                }
500
                for ($j = $min; $j <= $max; $j++) {
501
                    if (in_array($j, $lengthSet)) {
502
                        throw new \RuntimeException("Duplicate length element found ({$j}) in possibleLength string {$possibleLengthString}.");
503
                    }
504
                    $lengthSet[] = (int)$j;
505
                }
506
            } else {
507
                $length = $lengthSubstring;
508
                if (in_array($length, $lengthSet)) {
509
                    throw new \RuntimeException("Duplicate length element found ({$length}) in possibleLength string {$possibleLengthString}.");
510
                }
511
                if (!is_numeric($length)) {
512
                    throw new \RuntimeException("For input string \"{$length}\"");
513
                }
514
                $lengthSet[] = (int)$length;
515
            }
516
        }
517
        return $lengthSet;
518
    }
519
520
    /**
521
     * Reads the possible length present in the metadata and splits them into two sets: one for
522
     * full-length numbers, one for local numbers.
523
     *
524
     *
525
     * @param \DOMElement $data One or more phone number descriptions
526
     * @param array $lengths An array in which to add possible lengths of full phone numbers
527
     * @param array $localOnlyLengths An array in which to add possible lengths of phone numbers only diallable
528
     *  locally (e.g. within a province)
529
     */
530
    private static function populatePossibleLengthSets(\DOMElement $data, &$lengths, &$localOnlyLengths)
531
    {
532
        $possibleLengths = $data->getElementsByTagName(self::POSSIBLE_LENGTHS);
533
534
        for ($i = 0; $i < $possibleLengths->length; $i++) {
535
            /** @var \DOMElement $element */
536
            $element = $possibleLengths->item($i);
537
            $nationalLengths = $element->getAttribute(self::NATIONAL);
538
            // We don't add to the phone metadata yet, since we want to sort length elements found under
539
            // different nodes first, make sure there are no duplicates between them and that the
540
            // localOnly lengths don't overlap with the others.
541
            $thisElementLengths = self::parsePossibleLengthStringToSet($nationalLengths);
542
            if ($element->hasAttribute(self::LOCAL_ONLY)) {
543
                $localLengths = $element->getAttribute(self::LOCAL_ONLY);
544
                $thisElementLocalOnlyLengths = self::parsePossibleLengthStringToSet($localLengths);
545
                $intersection = array_intersect($thisElementLengths, $thisElementLocalOnlyLengths);
546
                if (count($intersection) > 0) {
547
                    throw new \RuntimeException("Possible length(s) found specified as a normal and local-only length: [" . implode(',', $intersection) . '].');
548
                }
549
                // We check again when we set these lengths on the metadata itself in setPossibleLengths
550
                // that the elements in localOnly are not also in lengths. For e.g. the generalDesc, it
551
                // might have a local-only length for one type that is a normal length for another type. We
552
                // don't consider this an error, but we do want to remove the local-only lengths.
553
                $localOnlyLengths = array_merge($localOnlyLengths, $thisElementLocalOnlyLengths);
554
                sort($localOnlyLengths);
555
            }
556
            // It is okay if at this time we have duplicates, because the same length might be possible
557
            // for e.g. fixed-line and for mobile numbers, and this method operates potentially on
558
            // multiple phoneNumberDesc XML elements.
559
            $lengths = array_merge($lengths, $thisElementLengths);
560
            sort($lengths);
561
        }
562
    }
563
564
    /**
565
     * Sets possible lengths in the general description, derived from certain child elements
566
     *
567
     * @internal
568
     * @param PhoneNumberDesc $generalDesc
569
     * @param string $metadataId
570
     * @param \DOMElement $data
571
     * @param bool $isShortNumberMetadata
572
     */
573
    public static function setPossibleLengthsGeneralDesc(PhoneNumberDesc $generalDesc, $metadataId, \DOMElement $data, $isShortNumberMetadata)
574
    {
575
        $lengths = array();
576
        $localOnlyLengths = array();
577
        // The general description node should *always* be present if metadata for other types is
578
        // present, aside from in some unit tests.
579
        // (However, for e.g. formatting metadata in PhoneNumberAlternateFormats, no PhoneNumberDesc
580
        // elements are present).
581
        $generalDescNodes = $data->getElementsByTagName(self::GENERAL_DESC);
582
        if ($generalDescNodes->length > 0) {
583
            $generalDescNode = $generalDescNodes->item(0);
584
            self::populatePossibleLengthSets($generalDescNode, $lengths, $localOnlyLengths);
585
            if (count($lengths) > 0 || count($localOnlyLengths) > 0) {
586
                // We shouldn't have anything specified at the "general desc" level: we are going to
587
                // calculate this ourselves from child elements.
588
                throw new \RuntimeException("Found possible lengths specified at general desc: this should be derived from child elements. Affected country: {$metadataId}");
589
            }
590
        }
591
        if (!$isShortNumberMetadata) {
592
            // Make a copy here since we want to remove some nodes, but we don't want to do that on our
593
            // actual data.
594
            /** @var \DOMElement $allDescData */
595
            $allDescData = $data->cloneNode(true);
596
            foreach (self::$phoneNumberDescsWithoutMatchingTypes as $tag) {
597
                $nodesToRemove = $allDescData->getElementsByTagName($tag);
598
                if ($nodesToRemove->length > 0) {
599
                    // We check when we process phone number descriptions that there are only one of each
600
                    // type, so this is safe to do.
601
                    $allDescData->removeChild($nodesToRemove->item(0));
602
                }
603
            }
604
            self::populatePossibleLengthSets($allDescData, $lengths, $localOnlyLengths);
605
        } else {
606
            // For short number metadata, we want to copy the lengths from the "short code" section only.
607
            // This is because it's the more detailed validation pattern, it's not a sub-type of short
608
            // codes. The other lengths will be checked later to see that they are a sub-set of these
609
            // possible lengths.
610
            $shortCodeDescList = $data->getElementsByTagName(self::SHORT_CODE);
611
            if (count($shortCodeDescList) > 0) {
612
                $shortCodeDesc = $shortCodeDescList->item(0);
613
                self::populatePossibleLengthSets($shortCodeDesc, $lengths, $localOnlyLengths);
614
            }
615
            if (count($localOnlyLengths) > 0) {
616
                throw new \RuntimeException("Found local-only lengths in short-number metadata");
617
            }
618
        }
619
        self::setPossibleLengths($lengths, $localOnlyLengths, null, $generalDesc);
620
    }
621
622
    /**
623
     * Sets the possible length fields in the metadata from the sets of data passed in. Checks that
624
     * the length is covered by the "parent" phone number description element if one is present, and
625
     * if the lengths are exactly the same as this, they are not filled in for efficiency reasons.
626
     *
627
     * @param array $lengths
628
     * @param array $localOnlyLengths
629
     * @param PhoneNumberDesc $parentDesc
630
     * @param PhoneNumberDesc $desc
631
     */
632
    private static function setPossibleLengths($lengths, $localOnlyLengths, PhoneNumberDesc $parentDesc = null, PhoneNumberDesc $desc)
633
    {
634
        // We clear these fields since the metadata tends to inherit from the parent element for other
635
        // fields (via a mergeFrom).
636
        $desc->clearPossibleLength();
637
        $desc->clearPossibleLengthLocalOnly();
638
639
        // Only add the lengths to this sub-type if they aren't exactly the same as the possible
640
        // lengths in the general desc (for metadata size reasons).
641
        if ($parentDesc === null || !self::arePossibleLengthsEqual($lengths, $parentDesc)) {
642
            foreach ($lengths as $length) {
643
                if ($parentDesc === null || in_array($length, $parentDesc->getPossibleLength())) {
644
                    $desc->addPossibleLength($length);
645
                } else {
646
                    // We shouldn't have possible lengths defined in a child element that are not covered by
647
                    // the general description. We check this here even though the general description is
648
                    // derived from child elements because it is only derived from a subset, and we need to
649
                    // ensure *all* child elements have a valid possible length.
650
                    throw new \RuntimeException("Out-of-range possible length found ({$length}), parent lengths " . implode(',', $parentDesc->getPossibleLength()));
651
                }
652
            }
653
        }
654
        // We check that the local-only length isn't also a normal possible length (only relevant for
655
        // the general-desc, since within elements such as fixed-line we would throw an exception if we
656
        // saw this) before adding it to the collection of possible local-only lengths.
657
        foreach ($localOnlyLengths as $length) {
658
            if (!in_array($length, $lengths)) {
659
                // We check it is covered by either of the possible length sets of the parent
660
                // PhoneNumberDesc, because for example 7 might be a valid localOnly length for mobile, but
661
                // a valid national length for fixedLine, so the generalDesc would have the 7 removed from
662
                // localOnly.
663
                if ($parentDesc === null
664
                    || in_array($length, $parentDesc->getPossibleLength())
665
                    || in_array($length, $parentDesc->getPossibleLengthLocalOnly())
666
                ) {
667
                    $desc->addPossibleLengthLocalOnly($length);
668
                } else {
669
                    throw new \RuntimeException("Out-of-range local-only possible length found ({$length}), parent length {$parentDesc->getPossibleLengthLocalOnly()}");
670
                }
671
            }
672
        }
673
    }
674
675
    /**
676
     * Processes a phone number description element from the XML file and returns it as a
677
     * PhoneNumberDesc. If the description element is a fixed line or mobile number, the parent
678
     * description will be used to fill in the whole element if necessary, or any components that are
679
     * missing. For all other types, the parent description will only be used to fill in missing
680
     * components if the type has a partial definition. For example, if no "tollFree" element exists,
681
     * we assume there are no toll free numbers for that locale, and return a phone number description
682
     * with no national number data and [-1] for this possible lengths. Note that the parent
683
     * description must therefore already be processed before this method is called on any child
684
     * elements.
685
     *
686
     * @internal
687
     * @param PhoneNumberDesc $parentDesc a generic phone number description that will be used to fill in missing
688
     *     parts of the description, or null if this is the root node. This must be processed before
689
     *     this is run on any child elements.
690
     * @param \DOMElement $countryElement XML element representing all the country information
691
     * @param string $numberType name of the number type, corresponding to the appropriate tag in the XML
692
     * file with information about that type
693
     * @return PhoneNumberDesc complete description of that phone number type
694
     */
695
    public static function processPhoneNumberDescElement(
696
        PhoneNumberDesc $parentDesc = null,
697
        \DOMElement $countryElement,
698
        $numberType
699
    ) {
700
        $phoneNumberDescList = $countryElement->getElementsByTagName($numberType);
701
        $numberDesc = new PhoneNumberDesc();
702
        if ($phoneNumberDescList->length == 0) {
703
            // -1 will never match a possible phone number length, so is safe to use to ensure this never
704
            // matches. We don't leave it empty, since for compression reasons, we use the empty list to
705
            // mean that the generalDesc possible lengths apply.
706
            $numberDesc->setPossibleLength(array(-1));
707
            return $numberDesc;
708
        }
709
710
        if ($parentDesc != null) {
711
            if ($parentDesc->getNationalNumberPattern() !== "") {
712
                $numberDesc->setNationalNumberPattern($parentDesc->getNationalNumberPattern());
713
            }
714
            if ($parentDesc->getExampleNumber() !== "") {
715
                $numberDesc->setExampleNumber($parentDesc->getExampleNumber());
716
            }
717
        }
718
719
        if ($phoneNumberDescList->length > 0) {
720
            if ($phoneNumberDescList->length > 1) {
721
                throw new \RuntimeException("Multiple elements with type {$numberType} found.");
722
            }
723
724
            /** @var \DOMElement $element */
725
            $element = $phoneNumberDescList->item(0);
726
727
            if ($parentDesc != null) {
728
                // New way of handling possible number lengths. We don't do this for the general
729
                // description, since these tags won't be present; instead we will calculate its values
730
                // based on the values for all the other number type descriptions (see
731
                // setPossibleLengthsGeneralDesc).
732
                $lengths = array();
733
                $localOnlyLengths = array();
734
                self::populatePossibleLengthSets($element, $lengths, $localOnlyLengths);
735
                self::setPossibleLengths($lengths, $localOnlyLengths, $parentDesc, $numberDesc);
736
            }
737
738
            $validPattern = $element->getElementsByTagName(self::NATIONAL_NUMBER_PATTERN);
739
            if ($validPattern->length > 0) {
740
                $numberDesc->setNationalNumberPattern(self::validateRE($validPattern->item(0)->firstChild->nodeValue, true));
741
            }
742
743
            $exampleNumber = $element->getElementsByTagName(self::EXAMPLE_NUMBER);
744
            if ($exampleNumber->length > 0) {
745
                $numberDesc->setExampleNumber($exampleNumber->item(0)->firstChild->nodeValue);
746
            }
747
        }
748
        return $numberDesc;
749
    }
750
751
    private static function arePossibleLengthsEqual($possibleLengths, PhoneNumberDesc $desc)
752
    {
753
        $descPossibleLength = $desc->getPossibleLength();
754
        if (count($possibleLengths) != count($descPossibleLength)) {
755
            return false;
756
        }
757
758
        // Note that both should be sorted already, and we know they are the same length.
759
        $i = 0;
760
        foreach ($possibleLengths as $length) {
761
            if ($length != $descPossibleLength[$i]) {
762
                return false;
763
            }
764
            $i++;
765
        }
766
        return true;
767
    }
768
769
    /**
770
     * @param PhoneMetadata[] $metadataCollection
771
     * @return array
772
     */
773
    public static function buildCountryCodeToRegionCodeMap($metadataCollection)
774
    {
775
        $countryCodeToRegionCodeMap = array();
776
777
        foreach ($metadataCollection as $metadata) {
778
            $regionCode = $metadata->getId();
779
            $countryCode = $metadata->getCountryCode();
780
            if (array_key_exists($countryCode, $countryCodeToRegionCodeMap)) {
781
                if ($metadata->getMainCountryForCode()) {
782
                    array_unshift($countryCodeToRegionCodeMap[$countryCode], $regionCode);
783
                } else {
784
                    $countryCodeToRegionCodeMap[$countryCode][] = $regionCode;
785
                }
786
            } else {
787
                // For most countries, there will be only one region code for the country calling code.
788
                $listWithRegionCode = array();
789
                if ($regionCode != '') { // For alternate formats, there are no region codes at all.
790
                    $listWithRegionCode[] = $regionCode;
791
                }
792
                $countryCodeToRegionCodeMap[$countryCode] = $listWithRegionCode;
793
            }
794
        }
795
796
        return $countryCodeToRegionCodeMap;
797
    }
798
}
799