Total Complexity | 107 |
Total Lines | 782 |
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) |
||
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) |
||
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( |
||
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) |
||
797 | } |
||
798 | } |
||
799 |