Total Complexity | 107 |
Total Lines | 792 |
Duplicated Lines | 0 % |
Changes | 0 |
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 |
||
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) { |
||
|
|||
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); |
||
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) |
||
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( |
||
194 | array('$NP', '$FG'), |
||
195 | array($nationalPrefix, '$1'), |
||
196 | $nationalPrefixFormattingRule |
||
197 | ); |
||
198 | return $nationalPrefixFormattingRule; |
||
199 | } |
||
200 | |||
201 | /** |
||
202 | * |
||
203 | * @internal |
||
204 | * @param string $regionCode |
||
205 | * @param \DOMElement $element |
||
206 | * @param string $nationalPrefix |
||
207 | * @return PhoneMetadata |
||
208 | */ |
||
209 | public static function loadTerritoryTagMetadata( |
||
210 | $regionCode, |
||
211 | \DOMElement $element, |
||
212 | $nationalPrefix |
||
213 | ) { |
||
214 | $metadata = new PhoneMetadata(); |
||
215 | $metadata->setId($regionCode); |
||
216 | $metadata->setCountryCode((int)$element->getAttribute(self::COUNTRY_CODE)); |
||
217 | if ($element->hasAttribute(self::LEADING_DIGITS)) { |
||
218 | $metadata->setLeadingDigits(self::validateRE($element->getAttribute(self::LEADING_DIGITS))); |
||
219 | } |
||
220 | $metadata->setInternationalPrefix(self::validateRE($element->getAttribute(self::INTERNATIONAL_PREFIX))); |
||
221 | if ($element->hasAttribute(self::PREFERRED_INTERNATIONAL_PREFIX)) { |
||
222 | $preferredInternationalPrefix = $element->getAttribute(self::PREFERRED_INTERNATIONAL_PREFIX); |
||
223 | $metadata->setPreferredInternationalPrefix($preferredInternationalPrefix); |
||
224 | } |
||
225 | if ($element->hasAttribute(self::NATIONAL_PREFIX_FOR_PARSING)) { |
||
226 | $metadata->setNationalPrefixForParsing( |
||
227 | self::validateRE($element->getAttribute(self::NATIONAL_PREFIX_FOR_PARSING), true) |
||
228 | ); |
||
229 | if ($element->hasAttribute(self::NATIONAL_PREFIX_TRANSFORM_RULE)) { |
||
230 | $metadata->setNationalPrefixTransformRule(self::validateRE($element->getAttribute(self::NATIONAL_PREFIX_TRANSFORM_RULE))); |
||
231 | } |
||
232 | } |
||
233 | if ($nationalPrefix != '') { |
||
234 | $metadata->setNationalPrefix($nationalPrefix); |
||
235 | if (!$metadata->hasNationalPrefixForParsing()) { |
||
236 | $metadata->setNationalPrefixForParsing($nationalPrefix); |
||
237 | } |
||
238 | } |
||
239 | if ($element->hasAttribute(self::PREFERRED_EXTN_PREFIX)) { |
||
240 | $metadata->setPreferredExtnPrefix($element->getAttribute(self::PREFERRED_EXTN_PREFIX)); |
||
241 | } |
||
242 | if ($element->hasAttribute(self::MAIN_COUNTRY_FOR_CODE)) { |
||
243 | $metadata->setMainCountryForCode(true); |
||
244 | } |
||
245 | if ($element->hasAttribute(self::MOBILE_NUMBER_PORTABLE_REGION)) { |
||
246 | $metadata->setMobileNumberPortableRegion(true); |
||
247 | } |
||
248 | return $metadata; |
||
249 | } |
||
250 | |||
251 | /** |
||
252 | * Extracts the available formats from the provided DOM element. If it does not contain any |
||
253 | * nationalPrefixFormattingRule, the one passed-in is retained; similarly for |
||
254 | * nationalPrefixOptionalWhenFormatting. The nationalPrefix, nationalPrefixFormattingRule and |
||
255 | * nationalPrefixOptionalWhenFormatting values are provided from the parent (territory) element. |
||
256 | * @internal |
||
257 | * @param PhoneMetadata $metadata |
||
258 | * @param \DOMElement $element |
||
259 | * @param string $nationalPrefix |
||
260 | * @param string $nationalPrefixFormattingRule |
||
261 | * @param bool $nationalPrefixOptionalWhenFormatting |
||
262 | */ |
||
263 | public static function loadAvailableFormats( |
||
264 | PhoneMetadata $metadata, |
||
265 | \DOMElement $element, |
||
266 | $nationalPrefix, |
||
267 | $nationalPrefixFormattingRule, |
||
268 | $nationalPrefixOptionalWhenFormatting |
||
269 | ) { |
||
270 | $carrierCodeFormattingRule = ''; |
||
271 | if ($element->hasAttribute(self::CARRIER_CODE_FORMATTING_RULE)) { |
||
272 | $carrierCodeFormattingRule = self::validateRE(self::getDomesticCarrierCodeFormattingRuleFromElement($element, $nationalPrefix)); |
||
273 | } |
||
274 | $numberFormatElements = $element->getElementsByTagName(self::NUMBER_FORMAT); |
||
275 | $hasExplicitIntlFormatDefined = false; |
||
276 | |||
277 | $numOfFormatElements = $numberFormatElements->length; |
||
278 | if ($numOfFormatElements > 0) { |
||
279 | for ($i = 0; $i < $numOfFormatElements; $i++) { |
||
280 | /** @var \DOMElement $numberFormatElement */ |
||
281 | $numberFormatElement = $numberFormatElements->item($i); |
||
282 | $format = new NumberFormat(); |
||
283 | |||
284 | if ($numberFormatElement->hasAttribute(self::NATIONAL_PREFIX_FORMATTING_RULE)) { |
||
285 | $format->setNationalPrefixFormattingRule( |
||
286 | self::getNationalPrefixFormattingRuleFromElement($numberFormatElement, $nationalPrefix) |
||
287 | ); |
||
288 | } else { |
||
289 | $format->setNationalPrefixFormattingRule($nationalPrefixFormattingRule); |
||
290 | } |
||
291 | if ($numberFormatElement->hasAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING)) { |
||
292 | $format->setNationalPrefixOptionalWhenFormatting($numberFormatElement->getAttribute(self::NATIONAL_PREFIX_OPTIONAL_WHEN_FORMATTING) === 'true' ? true : false); |
||
293 | } else { |
||
294 | $format->setNationalPrefixOptionalWhenFormatting($nationalPrefixOptionalWhenFormatting); |
||
295 | } |
||
296 | if ($numberFormatElement->hasAttribute(self::CARRIER_CODE_FORMATTING_RULE)) { |
||
297 | $format->setDomesticCarrierCodeFormattingRule( |
||
298 | self::validateRE(self::getDomesticCarrierCodeFormattingRuleFromElement($numberFormatElement, $nationalPrefix)) |
||
299 | ); |
||
300 | } else { |
||
301 | $format->setDomesticCarrierCodeFormattingRule($carrierCodeFormattingRule); |
||
302 | } |
||
303 | self::loadNationalFormat($metadata, $numberFormatElement, $format); |
||
304 | $metadata->addNumberFormat($format); |
||
305 | |||
306 | if (self::loadInternationalFormat($metadata, $numberFormatElement, $format)) { |
||
307 | $hasExplicitIntlFormatDefined = true; |
||
308 | } |
||
309 | } |
||
310 | // Only a small number of regions need to specify the intlFormats in the xml. For the majority |
||
311 | // of countries the intlNumberFormat metadata is an exact copy of the national NumberFormat |
||
312 | // metadata. To minimize the size of the metadata file, we only keep intlNumberFormats that |
||
313 | // actually differ in some way to the national formats. |
||
314 | if (!$hasExplicitIntlFormatDefined) { |
||
315 | $metadata->clearIntlNumberFormat(); |
||
316 | } |
||
317 | } |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * @internal |
||
322 | * @param \DOMElement $element |
||
323 | * @param string $nationalPrefix |
||
324 | * @return string |
||
325 | */ |
||
326 | public static function getDomesticCarrierCodeFormattingRuleFromElement(\DOMElement $element, $nationalPrefix) |
||
327 | { |
||
328 | $carrierCodeFormattingRule = $element->getAttribute(self::CARRIER_CODE_FORMATTING_RULE); |
||
329 | // Replace $FG with the first group ($1) and $NP with the national prefix. |
||
330 | $carrierCodeFormattingRule = \str_replace( |
||
331 | array('$NP', '$FG'), |
||
332 | array($nationalPrefix, '$1'), |
||
333 | $carrierCodeFormattingRule |
||
334 | ); |
||
335 | return $carrierCodeFormattingRule; |
||
336 | } |
||
337 | |||
338 | /** |
||
339 | * Extracts the pattern for the national format. |
||
340 | * |
||
341 | * @internal |
||
342 | * @param PhoneMetadata $metadata |
||
343 | * @param \DOMElement $numberFormatElement |
||
344 | * @param NumberFormat $format |
||
345 | * @throws \RuntimeException if multiple or no formats have been encountered. |
||
346 | */ |
||
347 | public static function loadNationalFormat( |
||
348 | PhoneMetadata $metadata, |
||
349 | \DOMElement $numberFormatElement, |
||
350 | NumberFormat $format |
||
351 | ) { |
||
352 | self::setLeadingDigitsPatterns($numberFormatElement, $format); |
||
353 | $format->setPattern(self::validateRE($numberFormatElement->getAttribute(self::PATTERN))); |
||
354 | |||
355 | $formatPattern = $numberFormatElement->getElementsByTagName(self::FORMAT); |
||
356 | if ($formatPattern->length != 1) { |
||
357 | $countryId = $metadata->getId() != '' ? $metadata->getId() : $metadata->getCountryCode(); |
||
358 | throw new \RuntimeException('Invalid number of format patterns for country: ' . $countryId); |
||
359 | } |
||
360 | $nationalFormat = $formatPattern->item(0)->firstChild->nodeValue; |
||
361 | $format->setFormat($nationalFormat); |
||
362 | } |
||
363 | |||
364 | /** |
||
365 | * @internal |
||
366 | * @param \DOMElement $numberFormatElement |
||
367 | * @param NumberFormat $format |
||
368 | */ |
||
369 | public static function setLeadingDigitsPatterns(\DOMElement $numberFormatElement, NumberFormat $format) |
||
370 | { |
||
371 | $leadingDigitsPatternNodes = $numberFormatElement->getElementsByTagName(self::LEADING_DIGITS); |
||
372 | $numOfLeadingDigitsPatterns = $leadingDigitsPatternNodes->length; |
||
373 | if ($numOfLeadingDigitsPatterns > 0) { |
||
374 | for ($i = 0; $i < $numOfLeadingDigitsPatterns; $i++) { |
||
375 | $format->addLeadingDigitsPattern(self::validateRE($leadingDigitsPatternNodes->item($i)->firstChild->nodeValue, true)); |
||
376 | } |
||
377 | } |
||
378 | } |
||
379 | |||
380 | /** |
||
381 | * Extracts the pattern for international format. If there is no intlFormat, default to using the |
||
382 | * national format. If the intlFormat is set to "NA" the intlFormat should be ignored. |
||
383 | * |
||
384 | * @internal |
||
385 | * @param PhoneMetadata $metadata |
||
386 | * @param \DOMElement $numberFormatElement |
||
387 | * @param NumberFormat $nationalFormat |
||
388 | * @throws \RuntimeException if multiple intlFormats have been encountered. |
||
389 | * @return bool whether an international number format is defined. |
||
390 | */ |
||
391 | public static function loadInternationalFormat( |
||
392 | PhoneMetadata $metadata, |
||
393 | \DOMElement $numberFormatElement, |
||
394 | NumberFormat $nationalFormat |
||
395 | ) { |
||
396 | $intlFormat = new NumberFormat(); |
||
397 | $intlFormatPattern = $numberFormatElement->getElementsByTagName(self::INTL_FORMAT); |
||
398 | $hasExplicitIntlFormatDefined = false; |
||
399 | |||
400 | if ($intlFormatPattern->length > 1) { |
||
401 | $countryId = $metadata->getId() != '' ? $metadata->getId() : $metadata->getCountryCode(); |
||
402 | throw new \RuntimeException('Invalid number of intlFormat patterns for country: ' . $countryId); |
||
403 | } |
||
404 | |||
405 | if ($intlFormatPattern->length === 0) { |
||
406 | // Default to use the same as the national pattern if none is defined. |
||
407 | $intlFormat->mergeFrom($nationalFormat); |
||
408 | } else { |
||
409 | $intlFormat->setPattern($numberFormatElement->getAttribute(self::PATTERN)); |
||
410 | self::setLeadingDigitsPatterns($numberFormatElement, $intlFormat); |
||
411 | $intlFormatPatternValue = $intlFormatPattern->item(0)->firstChild->nodeValue; |
||
412 | if ($intlFormatPatternValue !== 'NA') { |
||
413 | $intlFormat->setFormat($intlFormatPatternValue); |
||
414 | } |
||
415 | $hasExplicitIntlFormatDefined = true; |
||
416 | } |
||
417 | |||
418 | if ($intlFormat->hasFormat()) { |
||
419 | $metadata->addIntlNumberFormat($intlFormat); |
||
420 | } |
||
421 | return $hasExplicitIntlFormatDefined; |
||
422 | } |
||
423 | |||
424 | /** |
||
425 | * @internal |
||
426 | * @param PhoneMetadata $metadata |
||
427 | * @param \DOMElement $element |
||
428 | * @param bool $isShortNumberMetadata |
||
429 | */ |
||
430 | public static function setRelevantDescPatterns(PhoneMetadata $metadata, \DOMElement $element, $isShortNumberMetadata) |
||
463 | } |
||
464 | } |
||
465 | |||
466 | /** |
||
467 | * Parses a possible length string into a set of the integers that are covered. |
||
468 | * |
||
469 | * @param string $possibleLengthString a string specifying the possible lengths of phone numbers. Follows |
||
470 | * this syntax: ranges or elements are separated by commas, and ranges are specified in |
||
471 | * [min-max] notation, inclusive. For example, [3-5],7,9,[11-14] should be parsed to |
||
472 | * 3,4,5,7,9,11,12,13,14 |
||
473 | * @return array |
||
474 | */ |
||
475 | private static function parsePossibleLengthStringToSet($possibleLengthString) |
||
476 | { |
||
477 | if (\strlen($possibleLengthString) === 0) { |
||
478 | throw new \RuntimeException('Empty possibleLength string found.'); |
||
479 | } |
||
480 | |||
481 | $lengths = \explode(',', $possibleLengthString); |
||
482 | $lengthSet = array(); |
||
483 | |||
484 | |||
485 | $lengthLength = \count($lengths); |
||
486 | for ($i = 0; $i < $lengthLength; $i++) { |
||
487 | $lengthSubstring = $lengths[$i]; |
||
488 | if (\strlen($lengthSubstring) === 0) { |
||
489 | throw new \RuntimeException('Leading, trailing or adjacent commas in possible ' |
||
490 | . "length string {$possibleLengthString}, these should only separate numbers or ranges."); |
||
491 | } |
||
492 | |||
493 | if (\substr($lengthSubstring, 0, 1) === '[') { |
||
494 | if (\substr($lengthSubstring, -1) !== ']') { |
||
495 | throw new \RuntimeException("Missing end of range character in possible length string {$possibleLengthString}."); |
||
496 | } |
||
497 | |||
498 | // Strip the leading and trailing [], and split on the -. |
||
499 | $minMax = \explode('-', \substr($lengthSubstring, 1, -1)); |
||
500 | if (\count($minMax) !== 2) { |
||
501 | throw new \RuntimeException("Ranges must have exactly one - character: missing for {$possibleLengthString}."); |
||
502 | } |
||
503 | $min = (int)$minMax[0]; |
||
504 | $max = (int)$minMax[1]; |
||
505 | // We don't even accept [6-7] since we prefer the shorter 6,7 variant; for a range to be in |
||
506 | // use the hyphen needs to replace at least one digit. |
||
507 | if ($max - $min < 2) { |
||
508 | throw new \RuntimeException("The first number in a range should be two or more digits lower than the second. Culprit possibleLength string: {$possibleLengthString}."); |
||
509 | } |
||
510 | for ($j = $min; $j <= $max; $j++) { |
||
511 | if (\in_array($j, $lengthSet)) { |
||
512 | throw new \RuntimeException("Duplicate length element found ({$j}) in possibleLength string {$possibleLengthString}."); |
||
513 | } |
||
514 | $lengthSet[] = $j; |
||
515 | } |
||
516 | } else { |
||
517 | $length = $lengthSubstring; |
||
518 | if (\in_array($length, $lengthSet)) { |
||
519 | throw new \RuntimeException("Duplicate length element found ({$length}) in possibleLength string {$possibleLengthString}."); |
||
520 | } |
||
521 | if (!\is_numeric($length)) { |
||
522 | throw new \RuntimeException("For input string \"{$length}\""); |
||
523 | } |
||
524 | $lengthSet[] = (int)$length; |
||
525 | } |
||
526 | } |
||
527 | return $lengthSet; |
||
528 | } |
||
529 | |||
530 | /** |
||
531 | * Reads the possible length present in the metadata and splits them into two sets: one for |
||
532 | * full-length numbers, one for local numbers. |
||
533 | * |
||
534 | * |
||
535 | * @param \DOMElement $data One or more phone number descriptions |
||
536 | * @param array $lengths An array in which to add possible lengths of full phone numbers |
||
537 | * @param array $localOnlyLengths An array in which to add possible lengths of phone numbers only diallable |
||
538 | * locally (e.g. within a province) |
||
539 | */ |
||
540 | private static function populatePossibleLengthSets(\DOMElement $data, &$lengths, &$localOnlyLengths) |
||
541 | { |
||
542 | $possibleLengths = $data->getElementsByTagName(self::POSSIBLE_LENGTHS); |
||
543 | |||
544 | for ($i = 0; $i < $possibleLengths->length; $i++) { |
||
545 | /** @var \DOMElement $element */ |
||
546 | $element = $possibleLengths->item($i); |
||
547 | $nationalLengths = $element->getAttribute(self::NATIONAL); |
||
548 | // We don't add to the phone metadata yet, since we want to sort length elements found under |
||
549 | // different nodes first, make sure there are no duplicates between them and that the |
||
550 | // localOnly lengths don't overlap with the others. |
||
551 | $thisElementLengths = self::parsePossibleLengthStringToSet($nationalLengths); |
||
552 | if ($element->hasAttribute(self::LOCAL_ONLY)) { |
||
553 | $localLengths = $element->getAttribute(self::LOCAL_ONLY); |
||
554 | $thisElementLocalOnlyLengths = self::parsePossibleLengthStringToSet($localLengths); |
||
555 | $intersection = \array_intersect($thisElementLengths, $thisElementLocalOnlyLengths); |
||
556 | if (\count($intersection) > 0) { |
||
557 | throw new \RuntimeException('Possible length(s) found specified as a normal and local-only length: [' . \implode(',', $intersection) . '].'); |
||
558 | } |
||
559 | // We check again when we set these lengths on the metadata itself in setPossibleLengths |
||
560 | // that the elements in localOnly are not also in lengths. For e.g. the generalDesc, it |
||
561 | // might have a local-only length for one type that is a normal length for another type. We |
||
562 | // don't consider this an error, but we do want to remove the local-only lengths. |
||
563 | $localOnlyLengths = \array_merge($localOnlyLengths, $thisElementLocalOnlyLengths); |
||
564 | \sort($localOnlyLengths); |
||
565 | } |
||
566 | // It is okay if at this time we have duplicates, because the same length might be possible |
||
567 | // for e.g. fixed-line and for mobile numbers, and this method operates potentially on |
||
568 | // multiple phoneNumberDesc XML elements. |
||
569 | $lengths = \array_merge($lengths, $thisElementLengths); |
||
570 | \sort($lengths); |
||
571 | } |
||
572 | } |
||
573 | |||
574 | /** |
||
575 | * Sets possible lengths in the general description, derived from certain child elements |
||
576 | * |
||
577 | * @internal |
||
578 | * @param PhoneNumberDesc $generalDesc |
||
579 | * @param string $metadataId |
||
580 | * @param \DOMElement $data |
||
581 | * @param bool $isShortNumberMetadata |
||
582 | */ |
||
583 | public static function setPossibleLengthsGeneralDesc(PhoneNumberDesc $generalDesc, $metadataId, \DOMElement $data, $isShortNumberMetadata) |
||
584 | { |
||
585 | $lengths = array(); |
||
586 | $localOnlyLengths = array(); |
||
587 | // The general description node should *always* be present if metadata for other types is |
||
588 | // present, aside from in some unit tests. |
||
589 | // (However, for e.g. formatting metadata in PhoneNumberAlternateFormats, no PhoneNumberDesc |
||
590 | // elements are present). |
||
591 | $generalDescNodes = $data->getElementsByTagName(self::GENERAL_DESC); |
||
592 | if ($generalDescNodes->length > 0) { |
||
593 | $generalDescNode = $generalDescNodes->item(0); |
||
594 | self::populatePossibleLengthSets($generalDescNode, $lengths, $localOnlyLengths); |
||
595 | if (\count($lengths) > 0 || \count($localOnlyLengths) > 0) { |
||
596 | // We shouldn't have anything specified at the "general desc" level: we are going to |
||
597 | // calculate this ourselves from child elements. |
||
598 | throw new \RuntimeException("Found possible lengths specified at general desc: this should be derived from child elements. Affected country: {$metadataId}"); |
||
599 | } |
||
600 | } |
||
601 | if (!$isShortNumberMetadata) { |
||
602 | // Make a copy here since we want to remove some nodes, but we don't want to do that on our |
||
603 | // actual data. |
||
604 | /** @var \DOMElement $allDescData */ |
||
605 | $allDescData = $data->cloneNode(true); |
||
606 | foreach (self::$phoneNumberDescsWithoutMatchingTypes as $tag) { |
||
607 | $nodesToRemove = $allDescData->getElementsByTagName($tag); |
||
608 | if ($nodesToRemove->length > 0) { |
||
609 | // We check when we process phone number descriptions that there are only one of each |
||
610 | // type, so this is safe to do. |
||
611 | $allDescData->removeChild($nodesToRemove->item(0)); |
||
612 | } |
||
613 | } |
||
614 | self::populatePossibleLengthSets($allDescData, $lengths, $localOnlyLengths); |
||
615 | } else { |
||
616 | // For short number metadata, we want to copy the lengths from the "short code" section only. |
||
617 | // This is because it's the more detailed validation pattern, it's not a sub-type of short |
||
618 | // codes. The other lengths will be checked later to see that they are a sub-set of these |
||
619 | // possible lengths. |
||
620 | $shortCodeDescList = $data->getElementsByTagName(self::SHORT_CODE); |
||
621 | if (\count($shortCodeDescList) > 0) { |
||
622 | $shortCodeDesc = $shortCodeDescList->item(0); |
||
623 | self::populatePossibleLengthSets($shortCodeDesc, $lengths, $localOnlyLengths); |
||
624 | } |
||
625 | if (\count($localOnlyLengths) > 0) { |
||
626 | throw new \RuntimeException('Found local-only lengths in short-number metadata'); |
||
627 | } |
||
628 | } |
||
629 | self::setPossibleLengths($lengths, $localOnlyLengths, null, $generalDesc); |
||
630 | } |
||
631 | |||
632 | /** |
||
633 | * Sets the possible length fields in the metadata from the sets of data passed in. Checks that |
||
634 | * the length is covered by the "parent" phone number description element if one is present, and |
||
635 | * if the lengths are exactly the same as this, they are not filled in for efficiency reasons. |
||
636 | * |
||
637 | * @param array $lengths |
||
638 | * @param array $localOnlyLengths |
||
639 | * @param PhoneNumberDesc $parentDesc |
||
640 | * @param PhoneNumberDesc $desc |
||
641 | */ |
||
642 | private static function setPossibleLengths($lengths, $localOnlyLengths, PhoneNumberDesc $parentDesc = null, PhoneNumberDesc $desc) |
||
643 | { |
||
644 | // We clear these fields since the metadata tends to inherit from the parent element for other |
||
645 | // fields (via a mergeFrom). |
||
646 | $desc->clearPossibleLength(); |
||
647 | $desc->clearPossibleLengthLocalOnly(); |
||
648 | |||
649 | // Only add the lengths to this sub-type if they aren't exactly the same as the possible |
||
650 | // lengths in the general desc (for metadata size reasons). |
||
651 | if ($parentDesc === null || !self::arePossibleLengthsEqual($lengths, $parentDesc)) { |
||
652 | foreach ($lengths as $length) { |
||
653 | if ($parentDesc === null || \in_array($length, $parentDesc->getPossibleLength())) { |
||
654 | $desc->addPossibleLength($length); |
||
655 | } else { |
||
656 | // We shouldn't have possible lengths defined in a child element that are not covered by |
||
657 | // the general description. We check this here even though the general description is |
||
658 | // derived from child elements because it is only derived from a subset, and we need to |
||
659 | // ensure *all* child elements have a valid possible length. |
||
660 | throw new \RuntimeException("Out-of-range possible length found ({$length}), parent lengths " . \implode(',', $parentDesc->getPossibleLength())); |
||
661 | } |
||
662 | } |
||
663 | } |
||
664 | // We check that the local-only length isn't also a normal possible length (only relevant for |
||
665 | // the general-desc, since within elements such as fixed-line we would throw an exception if we |
||
666 | // saw this) before adding it to the collection of possible local-only lengths. |
||
667 | foreach ($localOnlyLengths as $length) { |
||
668 | if (!\in_array($length, $lengths)) { |
||
669 | // We check it is covered by either of the possible length sets of the parent |
||
670 | // PhoneNumberDesc, because for example 7 might be a valid localOnly length for mobile, but |
||
671 | // a valid national length for fixedLine, so the generalDesc would have the 7 removed from |
||
672 | // localOnly. |
||
673 | if ($parentDesc === null |
||
674 | || \in_array($length, $parentDesc->getPossibleLength()) |
||
675 | || \in_array($length, $parentDesc->getPossibleLengthLocalOnly()) |
||
676 | ) { |
||
677 | $desc->addPossibleLengthLocalOnly($length); |
||
678 | } else { |
||
679 | throw new \RuntimeException("Out-of-range local-only possible length found ({$length}), parent length {$parentDesc->getPossibleLengthLocalOnly()}"); |
||
680 | } |
||
681 | } |
||
682 | } |
||
683 | } |
||
684 | |||
685 | /** |
||
686 | * Processes a phone number description element from the XML file and returns it as a |
||
687 | * PhoneNumberDesc. If the description element is a fixed line or mobile number, the parent |
||
688 | * description will be used to fill in the whole element if necessary, or any components that are |
||
689 | * missing. For all other types, the parent description will only be used to fill in missing |
||
690 | * components if the type has a partial definition. For example, if no "tollFree" element exists, |
||
691 | * we assume there are no toll free numbers for that locale, and return a phone number description |
||
692 | * with no national number data and [-1] for this possible lengths. Note that the parent |
||
693 | * description must therefore already be processed before this method is called on any child |
||
694 | * elements. |
||
695 | * |
||
696 | * @internal |
||
697 | * @param PhoneNumberDesc $parentDesc a generic phone number description that will be used to fill in missing |
||
698 | * parts of the description, or null if this is the root node. This must be processed before |
||
699 | * this is run on any child elements. |
||
700 | * @param \DOMElement $countryElement XML element representing all the country information |
||
701 | * @param string $numberType name of the number type, corresponding to the appropriate tag in the XML |
||
702 | * file with information about that type |
||
703 | * @return PhoneNumberDesc complete description of that phone number type |
||
704 | */ |
||
705 | public static function processPhoneNumberDescElement( |
||
759 | } |
||
760 | |||
761 | private static function arePossibleLengthsEqual($possibleLengths, PhoneNumberDesc $desc) |
||
762 | { |
||
763 | $descPossibleLength = $desc->getPossibleLength(); |
||
764 | if (\count($possibleLengths) != \count($descPossibleLength)) { |
||
765 | return false; |
||
766 | } |
||
767 | |||
768 | // Note that both should be sorted already, and we know they are the same length. |
||
769 | $i = 0; |
||
770 | foreach ($possibleLengths as $length) { |
||
771 | if ($length != $descPossibleLength[$i]) { |
||
772 | return false; |
||
773 | } |
||
774 | $i++; |
||
775 | } |
||
776 | return true; |
||
777 | } |
||
778 | |||
779 | /** |
||
780 | * @param PhoneMetadata[] $metadataCollection |
||
781 | * @return array |
||
782 | */ |
||
783 | public static function buildCountryCodeToRegionCodeMap($metadataCollection) |
||
807 | } |
||
808 | } |
||
809 |