1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace libphonenumber; |
4
|
|
|
|
5
|
|
|
use libphonenumber\Leniency\AbstractLeniency; |
6
|
|
|
|
7
|
|
|
/** |
8
|
|
|
* A class that finds and extracts telephone numbers from $text. |
9
|
|
|
* Instances can be created using PhoneNumberUtil::findNumbers() |
10
|
|
|
* |
11
|
|
|
* Vanity numbers (phone numbers using alphabetic digits such as '1-800-SIX-FLAGS' are |
12
|
|
|
* not found. |
13
|
|
|
* |
14
|
|
|
* @package libphonenumber |
15
|
|
|
*/ |
16
|
|
|
class PhoneNumberMatcher implements \Iterator |
17
|
|
|
{ |
18
|
|
|
protected static $initialized = false; |
19
|
|
|
|
20
|
|
|
/** |
21
|
|
|
* The phone number pattern used by $this->find(), similar to |
22
|
|
|
* PhoneNumberUtil::VALID_PHONE_NUMBER, but with the following differences: |
23
|
|
|
* <ul> |
24
|
|
|
* <li>All captures are limited in order to place an upper bound to the text matched by the |
25
|
|
|
* pattern. |
26
|
|
|
* <ul> |
27
|
|
|
* <li>Leading punctuation / plus signs are limited. |
28
|
|
|
* <li>Consecutive occurrences of punctuation are limited. |
29
|
|
|
* <li>Number of digits is limited. |
30
|
|
|
* </ul> |
31
|
|
|
* <li>No whitespace is allowed at the start or end. |
32
|
|
|
* <li>No alpha digits (vanity numbers such as 1-800-SIX-FLAGS) are currently supported. |
33
|
|
|
* </ul> |
34
|
|
|
* |
35
|
|
|
* @var string |
36
|
|
|
*/ |
37
|
|
|
protected static $pattern; |
38
|
|
|
|
39
|
|
|
/** |
40
|
|
|
* Matches strings that look like publication pages. Example: |
41
|
|
|
* <pre>Computing Complete Answers to Queries in the Presence of Limited Access Patterns. |
42
|
|
|
* Chen Li. VLDB J. 12(3): 211-227 (2003).</pre> |
43
|
|
|
* |
44
|
|
|
* The string "211-227 (2003)" is not a telephone number. |
45
|
|
|
* |
46
|
|
|
* @var string |
47
|
|
|
*/ |
48
|
|
|
protected static $pubPages = "\\d{1,5}-+\\d{1,5}\\s{0,4}\\(\\d{1,4}"; |
49
|
|
|
|
50
|
|
|
/** |
51
|
|
|
* Matches strings that look like dates using "/" as a separator. Examples 3/10/2011, 31/10/2011 or |
52
|
|
|
* 08/31/95. |
53
|
|
|
* |
54
|
|
|
* @var string |
55
|
|
|
*/ |
56
|
|
|
protected static $slashSeparatedDates = "(?:(?:[0-3]?\\d/[01]?\\d)|(?:[01]?\\d/[0-3]?\\d))/(?:[12]\\d)?\\d{2}"; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Matches timestamps. Examples: "2012-01-02 08:00". Note that the reg-ex does not include the |
60
|
|
|
* trailing ":\d\d" -- that is covered by timeStampsSuffix. |
61
|
|
|
* |
62
|
|
|
* @var string |
63
|
|
|
*/ |
64
|
|
|
protected static $timeStamps = "[12]\\d{3}[-/]?[01]\\d[-/]?[0-3]\\d +[0-2]\\d$"; |
65
|
|
|
protected static $timeStampsSuffix = ":[0-5]\\d"; |
66
|
|
|
|
67
|
|
|
/** |
68
|
|
|
* Pattern to check that brackets match. Opening brackets should be closed within a phone number. |
69
|
|
|
* This also checks that there is something inside the brackets. Having no brackets at all is also |
70
|
|
|
* fine. |
71
|
|
|
* |
72
|
|
|
* @var string |
73
|
|
|
*/ |
74
|
|
|
protected static $matchingBrackets; |
75
|
|
|
|
76
|
|
|
/** |
77
|
|
|
* Patterns used to extract phone numbers from a larger phone-number-like pattern. These are |
78
|
|
|
* ordered according to specificity. For example, white-space is last since that is frequently |
79
|
|
|
* used in numbers, not just to separate two numbers. We have separate patterns since we don't |
80
|
|
|
* want to break up the phone-number-like text on more than one different kind of symbol at one |
81
|
|
|
* time, although symbols of the same type (e.g. space) can be safely grouped together. |
82
|
|
|
* |
83
|
|
|
* Note that if there is a match, we will always check any text found up to the first match as |
84
|
|
|
* well. |
85
|
|
|
* |
86
|
|
|
* @var string[] |
87
|
|
|
*/ |
88
|
|
|
protected static $innerMatches = array(); |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Punctuation that may be at the start of a phone number - brackets and plus signs. |
92
|
|
|
* |
93
|
|
|
* @var string |
94
|
|
|
*/ |
95
|
|
|
protected static $leadClass; |
96
|
|
|
|
97
|
|
|
/** |
98
|
|
|
* Prefix of the files |
99
|
|
|
* @var string |
100
|
|
|
*/ |
101
|
|
|
protected static $alternateFormatsFilePrefix; |
102
|
|
|
const META_DATA_FILE_PREFIX = 'PhoneNumberAlternateFormats'; |
103
|
|
|
|
104
|
1 |
|
protected static function init() |
105
|
|
|
{ |
106
|
1 |
|
static::$alternateFormatsFilePrefix = \dirname(__FILE__) . '/data/' . static::META_DATA_FILE_PREFIX; |
107
|
|
|
|
108
|
1 |
|
static::$innerMatches = array( |
109
|
|
|
// Breaks on the slash - e.g. "651-234-2345/332-445-1234" |
110
|
|
|
'/+(.*)', |
111
|
|
|
// Note that the bracket here is inside the capturing group, since we consider it part of the |
112
|
|
|
// phone number. Will match a pattern like "(650) 223 3345 (754) 223 3321". |
113
|
|
|
"(\\([^(]*)", |
114
|
|
|
// Breaks on a hyphen - e.g. "12345 - 332-445-1234 is my number." |
115
|
|
|
// We require a space on either side of the hyphen for it to be considered a separator. |
116
|
|
|
"(?:\\p{Z}-|-\\p{Z})\\p{Z}*(.+)", |
117
|
|
|
// Various types of wide hyphens. Note we have decided not to enforce a space here, since it's |
118
|
|
|
// possible that it's supposed to be used to break two numbers without spaces, and we haven't |
119
|
|
|
// seen many instances of it used within a number. |
120
|
|
|
"[‒-―-]\\p{Z}*(.+)", |
121
|
|
|
// Breaks on a full stop - e.g. "12345. 332-445-1234 is my number." |
122
|
|
|
"\\.+\\p{Z}*([^.]+)", |
123
|
|
|
// Breaks on space - e.g. "3324451234 8002341234" |
124
|
|
|
"\\p{Z}+(\\P{Z}+)" |
125
|
|
|
); |
126
|
|
|
|
127
|
|
|
/* |
128
|
|
|
* Builds the matchingBrackets and pattern regular expressions. The building blocks exist |
129
|
|
|
* to make the pattern more easily understood. |
130
|
|
|
*/ |
131
|
|
|
|
132
|
1 |
|
$openingParens = "(\\[\xEF\xBC\x88\xEF\xBC\xBB"; |
133
|
1 |
|
$closingParens = ")\\]\xEF\xBC\x89\xEF\xBC\xBD"; |
134
|
1 |
|
$nonParens = '[^' . $openingParens . $closingParens . ']'; |
135
|
|
|
|
136
|
|
|
// Limit on the number of pairs of brackets in a phone number. |
137
|
1 |
|
$bracketPairLimit = static::limit(0, 3); |
138
|
|
|
|
139
|
|
|
/* |
140
|
|
|
* An opening bracket at the beginning may not be closed, but subsequent ones should be. It's |
141
|
|
|
* also possible that the leading bracket was dropped, so we shouldn't be surprised if we see a |
142
|
|
|
* closing bracket first. We limit the sets of brackets in a phone number to four. |
143
|
|
|
*/ |
144
|
1 |
|
static::$matchingBrackets = |
145
|
1 |
|
'(?:[' . $openingParens . '])?' . '(?:' . $nonParens . '+' . '[' . $closingParens . '])?' |
146
|
1 |
|
. $nonParens . '+' |
147
|
1 |
|
. '(?:[' . $openingParens . ']' . $nonParens . '+[' . $closingParens . '])' . $bracketPairLimit |
148
|
1 |
|
. $nonParens . '*'; |
149
|
|
|
|
150
|
|
|
// Limit on the number of leading (plus) characters. |
151
|
1 |
|
$leadLimit = static::limit(0, 2); |
152
|
|
|
|
153
|
|
|
// Limit on the number of consecutive punctuation characters. |
154
|
1 |
|
$punctuationLimit = static::limit(0, 4); |
155
|
|
|
|
156
|
|
|
/* |
157
|
|
|
* The maximum number of digits allowed in a digit-separated block. As we allow all digits in a |
158
|
|
|
* single block, set high enough to accommodate the entire national number and the international |
159
|
|
|
* country code |
160
|
|
|
*/ |
161
|
1 |
|
$digitBlockLimit = PhoneNumberUtil::MAX_LENGTH_FOR_NSN + PhoneNumberUtil::MAX_LENGTH_COUNTRY_CODE; |
162
|
|
|
|
163
|
|
|
/* |
164
|
|
|
* Limit on the number of blocks separated by the punctuation. Uses digitBlockLimit since some |
165
|
|
|
* formats use spaces to separate each digit |
166
|
|
|
*/ |
167
|
1 |
|
$blockLimit = static::limit(0, $digitBlockLimit); |
168
|
|
|
|
169
|
|
|
// A punctuation sequence allowing white space |
170
|
1 |
|
$punctuation = '[' . PhoneNumberUtil::VALID_PUNCTUATION . ']' . $punctuationLimit; |
171
|
|
|
|
172
|
|
|
// A digits block without punctuation. |
173
|
1 |
|
$digitSequence = "\\p{Nd}" . static::limit(1, $digitBlockLimit); |
174
|
|
|
|
175
|
|
|
|
176
|
1 |
|
$leadClassChars = $openingParens . PhoneNumberUtil::PLUS_CHARS; |
177
|
1 |
|
$leadClass = '[' . $leadClassChars . ']'; |
178
|
1 |
|
static::$leadClass = $leadClass; |
179
|
|
|
|
180
|
|
|
// Init extension patterns from PhoneNumberUtil |
181
|
1 |
|
PhoneNumberUtil::initExtnPatterns(); |
182
|
|
|
|
183
|
|
|
// Phone number pattern allowing optional punctuation. |
184
|
1 |
|
static::$pattern = '(?:' . $leadClass . $punctuation . ')' . $leadLimit |
185
|
1 |
|
. $digitSequence . '(?:' . $punctuation . $digitSequence . ')' . $blockLimit |
186
|
1 |
|
. '(?:' . PhoneNumberUtil::$EXTN_PATTERNS_FOR_MATCHING . ')?'; |
187
|
|
|
|
188
|
1 |
|
static::$initialized = true; |
189
|
1 |
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Helper function to generate regular expression with an upper and lower limit. |
193
|
|
|
* |
194
|
|
|
* @param int $lower |
195
|
|
|
* @param int $upper |
196
|
|
|
* @return string |
197
|
|
|
*/ |
198
|
1 |
|
protected static function limit($lower, $upper) |
199
|
|
|
{ |
200
|
1 |
|
if (($lower < 0) || ($upper <= 0) || ($upper < $lower)) { |
201
|
|
|
throw new \InvalidArgumentException(); |
202
|
|
|
} |
203
|
|
|
|
204
|
1 |
|
return '{' . $lower . ',' . $upper . '}'; |
205
|
|
|
} |
206
|
|
|
|
207
|
|
|
/** |
208
|
|
|
* The phone number utility. |
209
|
|
|
* @var PhoneNumberUtil |
210
|
|
|
*/ |
211
|
|
|
protected $phoneUtil; |
212
|
|
|
|
213
|
|
|
/** |
214
|
|
|
* The text searched for phone numbers. |
215
|
|
|
* @var string |
216
|
|
|
*/ |
217
|
|
|
protected $text; |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* The region (country) to assume for phone numbers without an international prefix, possibly |
221
|
|
|
* null. |
222
|
|
|
* @var string |
223
|
|
|
*/ |
224
|
|
|
protected $preferredRegion; |
225
|
|
|
|
226
|
|
|
/** |
227
|
|
|
* The degrees of validation requested. |
228
|
|
|
* @var AbstractLeniency |
229
|
|
|
*/ |
230
|
|
|
protected $leniency; |
231
|
|
|
|
232
|
|
|
/** |
233
|
|
|
* The maximum number of retires after matching an invalid number. |
234
|
|
|
* @var int |
235
|
|
|
*/ |
236
|
|
|
protected $maxTries; |
237
|
|
|
|
238
|
|
|
/** |
239
|
|
|
* One of: |
240
|
|
|
* - NOT_READY |
241
|
|
|
* - READY |
242
|
|
|
* - DONE |
243
|
|
|
* @var string |
244
|
|
|
*/ |
245
|
|
|
protected $state = 'NOT_READY'; |
246
|
|
|
|
247
|
|
|
/** |
248
|
|
|
* The last successful match, null unless $this->state = READY |
249
|
|
|
* @var PhoneNumberMatch |
250
|
|
|
*/ |
251
|
|
|
protected $lastMatch; |
252
|
|
|
|
253
|
|
|
/** |
254
|
|
|
* The next index to start searching at. Undefined when $this->state = DONE |
255
|
|
|
* @var int |
256
|
|
|
*/ |
257
|
|
|
protected $searchIndex = 0; |
258
|
|
|
|
259
|
|
|
/** |
260
|
|
|
* Creates a new instance. See the factory methods in PhoneNumberUtil on how to obtain a new instance. |
261
|
|
|
* |
262
|
|
|
* |
263
|
|
|
* @param PhoneNumberUtil $util The Phone Number Util to use |
264
|
|
|
* @param string|null $text The text that we will search, null for no text |
265
|
|
|
* @param string|null $country The country to assume for phone numbers not written in international format. |
266
|
|
|
* (with a leading plus, or with the international dialling prefix of the specified region). |
267
|
|
|
* May be null, or "ZZ" if only numbers with a leading plus should be considered. |
268
|
|
|
* @param AbstractLeniency $leniency The leniency to use when evaluating candidate phone numbers |
269
|
|
|
* @param int $maxTries The maximum number of invalid numbers to try before giving up on the text. |
270
|
|
|
* This is to cover degenerate cases where the text has a lot of false positives in it. Must be >= 0 |
271
|
|
|
* @throws \InvalidArgumentException |
272
|
|
|
*/ |
273
|
207 |
|
public function __construct(PhoneNumberUtil $util, $text, $country, AbstractLeniency $leniency, $maxTries) |
274
|
|
|
{ |
275
|
207 |
|
if ($maxTries < 0) { |
276
|
|
|
throw new \InvalidArgumentException(); |
277
|
|
|
} |
278
|
|
|
|
279
|
207 |
|
$this->phoneUtil = $util; |
280
|
207 |
|
$this->text = ($text !== null) ? $text : ''; |
281
|
207 |
|
$this->preferredRegion = $country; |
282
|
207 |
|
$this->leniency = $leniency; |
283
|
207 |
|
$this->maxTries = $maxTries; |
284
|
|
|
|
285
|
207 |
|
if (static::$initialized === false) { |
286
|
1 |
|
static::init(); |
287
|
|
|
} |
288
|
207 |
|
} |
289
|
|
|
|
290
|
|
|
/** |
291
|
|
|
* Attempts to find the next subsequence in the searched sequence on or after {@code searchIndex} |
292
|
|
|
* that represents a phone number. Returns the next match, null if none was found. |
293
|
|
|
* |
294
|
|
|
* @param int $index The search index to start searching at |
295
|
|
|
* @return PhoneNumberMatch|null The Phone Number Match found, null if none can be found |
296
|
|
|
*/ |
297
|
201 |
|
protected function find($index) |
298
|
|
|
{ |
299
|
201 |
|
$matcher = new Matcher(static::$pattern, $this->text); |
300
|
201 |
|
while (($this->maxTries > 0) && $matcher->find($index)) { |
301
|
200 |
|
$start = $matcher->start(); |
302
|
200 |
|
$cutLength = $matcher->end() - $start; |
303
|
200 |
|
$candidate = \mb_substr($this->text, $start, $cutLength); |
|
|
|
|
304
|
|
|
|
305
|
|
|
// Check for extra numbers at the end. |
306
|
|
|
// TODO: This is the place to start when trying to support extraction of multiple phone number |
307
|
|
|
// from split notations (+41 49 123 45 67 / 68). |
308
|
200 |
|
$candidate = static::trimAfterFirstMatch(PhoneNumberUtil::$SECOND_NUMBER_START_PATTERN, $candidate); |
309
|
|
|
|
310
|
200 |
|
$match = $this->extractMatch($candidate, $start); |
|
|
|
|
311
|
200 |
|
if ($match !== null) { |
312
|
126 |
|
return $match; |
313
|
|
|
} |
314
|
|
|
|
315
|
92 |
|
$index = $start + \mb_strlen($candidate); |
316
|
92 |
|
$this->maxTries--; |
317
|
|
|
} |
318
|
|
|
|
319
|
95 |
|
return null; |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Trims away any characters after the first match of $pattern in $candidate, |
324
|
|
|
* returning the trimmed version. |
325
|
|
|
* |
326
|
|
|
* @param string $pattern |
327
|
|
|
* @param string $candidate |
328
|
|
|
* @return string |
329
|
|
|
*/ |
330
|
200 |
|
protected static function trimAfterFirstMatch($pattern, $candidate) |
331
|
|
|
{ |
332
|
200 |
|
$trailingCharsMatcher = new Matcher($pattern, $candidate); |
333
|
200 |
|
if ($trailingCharsMatcher->find()) { |
334
|
10 |
|
$startChar = $trailingCharsMatcher->start(); |
335
|
10 |
|
$candidate = \mb_substr($candidate, 0, $startChar); |
336
|
|
|
} |
337
|
200 |
|
return $candidate; |
338
|
|
|
} |
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* Helper method to determine if a character is a Latin-script letter or not. For our purposes, |
342
|
|
|
* combining marks should also return true since we assume they have been added to a preceding |
343
|
|
|
* Latin character. |
344
|
|
|
* |
345
|
|
|
* @param string $letter |
346
|
|
|
* @return bool |
347
|
|
|
* @internal |
348
|
|
|
*/ |
349
|
60 |
|
public static function isLatinLetter($letter) |
350
|
|
|
{ |
351
|
|
|
// Combining marks are a subset of non-spacing-mark. |
352
|
60 |
|
if (\preg_match('/\p{L}/u', $letter) !== 1 && \preg_match('/\p{Mn}/u', $letter) !== 1) { |
353
|
54 |
|
return false; |
354
|
|
|
} |
355
|
|
|
|
356
|
9 |
|
return (\preg_match('/\p{Latin}/u', $letter) === 1) |
357
|
9 |
|
|| (\preg_match('/\pM+/u', $letter) === 1); |
358
|
|
|
} |
359
|
|
|
|
360
|
|
|
/** |
361
|
|
|
* @param string $character |
362
|
|
|
* @return bool |
363
|
|
|
*/ |
364
|
49 |
|
protected static function isInvalidPunctuationSymbol($character) |
365
|
|
|
{ |
366
|
49 |
|
return $character == '%' || \preg_match('/\p{Sc}/u', $character); |
367
|
|
|
} |
368
|
|
|
|
369
|
|
|
/** |
370
|
|
|
* Attempts to extract a match from a $candidate. |
371
|
|
|
* |
372
|
|
|
* @param string $candidate The candidate text that might contain a phone number |
373
|
|
|
* @param int $offset The offset of $candidate within $this->text |
374
|
|
|
* @return PhoneNumberMatch|null The match found, null if none can be found |
375
|
|
|
*/ |
376
|
200 |
|
protected function extractMatch($candidate, $offset) |
377
|
|
|
{ |
378
|
|
|
// Skip a match that is more likely to be a date. |
379
|
200 |
|
$dateMatcher = new Matcher(static::$slashSeparatedDates, $candidate); |
380
|
200 |
|
if ($dateMatcher->find()) { |
381
|
33 |
|
return null; |
382
|
|
|
} |
383
|
|
|
|
384
|
|
|
// Skip potential time-stamps. |
385
|
180 |
|
$timeStampMatcher = new Matcher(static::$timeStamps, $candidate); |
386
|
180 |
|
if ($timeStampMatcher->find()) { |
387
|
20 |
|
$followingText = \mb_substr($this->text, $offset + \mb_strlen($candidate)); |
388
|
20 |
|
$timeStampSuffixMatcher = new Matcher(static::$timeStampsSuffix, $followingText); |
389
|
20 |
|
if ($timeStampSuffixMatcher->lookingAt()) { |
390
|
16 |
|
return null; |
391
|
|
|
} |
392
|
|
|
} |
393
|
|
|
|
394
|
|
|
// Try to come up with a valid match given the entire candidate. |
395
|
180 |
|
$match = $this->parseAndVerify($candidate, $offset); |
|
|
|
|
396
|
180 |
|
if ($match !== null) { |
|
|
|
|
397
|
124 |
|
return $match; |
398
|
|
|
} |
399
|
|
|
|
400
|
|
|
// If that failed, try to find an "inner match" - there might be a phone number within this |
401
|
|
|
// candidate. |
402
|
76 |
|
return $this->extractInnerMatch($candidate, $offset); |
|
|
|
|
403
|
|
|
} |
404
|
|
|
|
405
|
|
|
/** |
406
|
|
|
* Attempts to extract a match from $candidate if the whole candidate does not qualify as a |
407
|
|
|
* match. |
408
|
|
|
* |
409
|
|
|
* @param string $candidate The candidate text that might contact a phone number |
410
|
|
|
* @param int $offset The current offset of $candidate within $this->text |
411
|
|
|
* @return PhoneNumberMatch|null The match found, null if none can be found |
412
|
|
|
*/ |
413
|
76 |
|
protected function extractInnerMatch($candidate, $offset) |
414
|
|
|
{ |
415
|
76 |
|
foreach (static::$innerMatches as $possibleInnerMatch) { |
416
|
76 |
|
$groupMatcher = new Matcher($possibleInnerMatch, $candidate); |
417
|
76 |
|
$isFirstMatch = true; |
418
|
|
|
|
419
|
76 |
|
while ($groupMatcher->find() && $this->maxTries > 0) { |
420
|
20 |
|
if ($isFirstMatch) { |
421
|
|
|
// We should handle any group before this one too. |
422
|
20 |
|
$group = static::trimAfterFirstMatch( |
423
|
20 |
|
PhoneNumberUtil::$UNWANTED_END_CHAR_PATTERN, |
424
|
20 |
|
\mb_substr($candidate, 0, $groupMatcher->start()) |
425
|
|
|
); |
426
|
|
|
|
427
|
20 |
|
$match = $this->parseAndVerify($group, $offset); |
|
|
|
|
428
|
20 |
|
if ($match !== null) { |
429
|
6 |
|
return $match; |
430
|
|
|
} |
431
|
17 |
|
$this->maxTries--; |
432
|
17 |
|
$isFirstMatch = false; |
433
|
|
|
} |
434
|
17 |
|
$group = static::trimAfterFirstMatch( |
435
|
17 |
|
PhoneNumberUtil::$UNWANTED_END_CHAR_PATTERN, |
436
|
17 |
|
$groupMatcher->group(1) |
437
|
|
|
); |
438
|
17 |
|
$match = $this->parseAndVerify($group, $offset + $groupMatcher->start(1)); |
|
|
|
|
439
|
17 |
|
if ($match !== null) { |
440
|
7 |
|
return $match; |
441
|
|
|
} |
442
|
16 |
|
$this->maxTries--; |
443
|
|
|
} |
444
|
|
|
} |
445
|
72 |
|
return null; |
446
|
|
|
} |
447
|
|
|
|
448
|
|
|
/** |
449
|
|
|
* Parses a phone number from the $candidate} using PhoneNumberUtil::parse() and |
450
|
|
|
* verifies it matches the requested leniency. If parsing and verification succeed, a |
451
|
|
|
* corresponding PhoneNumberMatch is returned, otherwise this method returns null. |
452
|
|
|
* |
453
|
|
|
* @param string $candidate The candidate match |
454
|
|
|
* @param int $offset The offset of $candidate within $this->text |
455
|
|
|
* @return PhoneNumberMatch|null The parsed and validated phone number match, or null |
456
|
|
|
*/ |
457
|
180 |
|
protected function parseAndVerify($candidate, $offset) |
458
|
|
|
{ |
459
|
|
|
try { |
460
|
|
|
// Check the candidate doesn't contain any formatting which would indicate that it really |
461
|
|
|
// isn't a phone number |
462
|
180 |
|
$matchingBracketsMatcher = new Matcher(static::$matchingBrackets, $candidate); |
463
|
180 |
|
$pubPagesMatcher = new Matcher(static::$pubPages, $candidate); |
464
|
180 |
|
if (!$matchingBracketsMatcher->matches() || $pubPagesMatcher->find()) { |
465
|
11 |
|
return null; |
466
|
|
|
} |
467
|
|
|
|
468
|
|
|
// If leniency is set to VALID or stricter, we also want to skip numbers that are surrounded |
469
|
|
|
// by Latin alphabetic characters, to skip cases like abc8005001234 or 8005001234def. |
470
|
180 |
|
if ($this->leniency->compareTo(Leniency::VALID()) >= 0) { |
471
|
|
|
// If the candidate is not at the start of the text, and does not start with phone-number |
472
|
|
|
// punctuation, check the previous character. |
473
|
137 |
|
$leadClassMatcher = new Matcher(static::$leadClass, $candidate); |
474
|
137 |
|
if ($offset > 0 && !$leadClassMatcher->lookingAt()) { |
475
|
44 |
|
$previousChar = \mb_substr($this->text, $offset - 1, 1); |
476
|
|
|
// We return null if it is a latin letter or an invalid punctuation symbol. |
477
|
44 |
|
if (static::isInvalidPunctuationSymbol($previousChar) || static::isLatinLetter($previousChar)) { |
478
|
2 |
|
return null; |
479
|
|
|
} |
480
|
|
|
} |
481
|
137 |
|
$lastCharIndex = $offset + \mb_strlen($candidate); |
482
|
137 |
|
if ($lastCharIndex < \mb_strlen($this->text)) { |
483
|
40 |
|
$nextChar = \mb_substr($this->text, $lastCharIndex, 1); |
484
|
40 |
|
if (static::isInvalidPunctuationSymbol($nextChar) || static::isLatinLetter($nextChar)) { |
485
|
2 |
|
return null; |
486
|
|
|
} |
487
|
|
|
} |
488
|
|
|
} |
489
|
|
|
|
490
|
179 |
|
$number = $this->phoneUtil->parseAndKeepRawInput($candidate, $this->preferredRegion); |
491
|
|
|
|
492
|
178 |
|
if ($this->leniency->verify($number, $candidate, $this->phoneUtil)) { |
493
|
|
|
// We used parseAndKeepRawInput to create this number, but for now we don't return the extra |
494
|
|
|
// values parsed. TODO: stop clearing all values here and switch all users over |
495
|
|
|
// to using rawInput() rather than the rawString() of PhoneNumberMatch |
496
|
126 |
|
$number->clearCountryCodeSource(); |
497
|
126 |
|
$number->clearRawInput(); |
498
|
126 |
|
$number->clearPreferredDomesticCarrierCode(); |
499
|
178 |
|
return new PhoneNumberMatch($offset, $candidate, $number); |
500
|
|
|
} |
501
|
28 |
|
} catch (NumberParseException $e) { |
502
|
|
|
// ignore and continue |
503
|
|
|
} |
504
|
74 |
|
return null; |
505
|
|
|
} |
506
|
|
|
|
507
|
|
|
/** |
508
|
|
|
* @param PhoneNumberUtil $util |
509
|
|
|
* @param PhoneNumber $number |
510
|
|
|
* @param string $normalizedCandidate |
511
|
|
|
* @param string[] $formattedNumberGroups |
512
|
|
|
* @return bool |
513
|
|
|
*/ |
514
|
27 |
|
public static function allNumberGroupsRemainGrouped( |
515
|
|
|
PhoneNumberUtil $util, |
516
|
|
|
PhoneNumber $number, |
517
|
|
|
$normalizedCandidate, |
518
|
|
|
$formattedNumberGroups |
519
|
|
|
) { |
520
|
27 |
|
$fromIndex = 0; |
521
|
27 |
|
if ($number->getCountryCodeSource() !== CountryCodeSource::FROM_DEFAULT_COUNTRY) { |
522
|
|
|
// First skip the country code if the normalized candidate contained it. |
523
|
11 |
|
$countryCode = $number->getCountryCode(); |
524
|
11 |
|
$fromIndex = \mb_strpos($normalizedCandidate, $countryCode) + \mb_strlen($countryCode); |
|
|
|
|
525
|
|
|
} |
526
|
|
|
|
527
|
|
|
// Check each group of consecutive digits are not broken into separate groupings in the |
528
|
|
|
// $normalizedCandidate string. |
529
|
27 |
|
$formattedNumberGroupsLength = \count($formattedNumberGroups); |
530
|
27 |
|
for ($i = 0; $i < $formattedNumberGroupsLength; $i++) { |
531
|
|
|
// Fails if the substring of $normalizedCandidate starting from $fromIndex |
532
|
|
|
// doesn't contain the consecutive digits in $formattedNumberGroups[$i]. |
533
|
27 |
|
$fromIndex = \mb_strpos($normalizedCandidate, $formattedNumberGroups[$i], $fromIndex); |
534
|
27 |
|
if ($fromIndex === false) { |
535
|
9 |
|
return false; |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
// Moves $fromIndex forward. |
539
|
26 |
|
$fromIndex += \mb_strlen($formattedNumberGroups[$i]); |
540
|
26 |
|
if ($i === 0 && $fromIndex < \mb_strlen($normalizedCandidate)) { |
541
|
|
|
// We are at the position right after the NDC. We get the region used for formatting |
542
|
|
|
// information based on the country code in the phone number, rather than the number itself, |
543
|
|
|
// as we do not need to distinguish between different countries with the same country |
544
|
|
|
// calling code and this is faster. |
545
|
26 |
|
$region = $util->getRegionCodeForCountryCode($number->getCountryCode()); |
546
|
|
|
|
547
|
26 |
|
if ($util->getNddPrefixForRegion($region, true) !== null |
548
|
26 |
|
&& \is_int(\mb_substr($normalizedCandidate, $fromIndex, 1)) |
549
|
|
|
) { |
550
|
|
|
// This means there is no formatting symbol after the NDC. In this case, we only |
551
|
|
|
// accept the number if there is no formatting symbol at all in the number, except |
552
|
|
|
// for extensions. This is only important for countries with national prefixes. |
553
|
|
|
$nationalSignificantNumber = $util->getNationalSignificantNumber($number); |
554
|
|
|
return \mb_substr( |
555
|
|
|
\mb_substr($normalizedCandidate, $fromIndex - \mb_strlen($formattedNumberGroups[$i])), |
556
|
|
|
\mb_strlen($nationalSignificantNumber) |
557
|
|
|
) === $nationalSignificantNumber; |
558
|
|
|
} |
559
|
|
|
} |
560
|
|
|
} |
561
|
|
|
// The check here makes sure that we haven't mistakenly already used the extension to |
562
|
|
|
// match the last group of the subscriber number. Note the extension cannot have |
563
|
|
|
// formatting in-between digits |
564
|
|
|
|
565
|
25 |
|
if ($number->hasExtension()) { |
566
|
4 |
|
return \mb_strpos(\mb_substr($normalizedCandidate, $fromIndex), $number->getExtension()) !== false; |
567
|
|
|
} |
568
|
|
|
|
569
|
21 |
|
return true; |
570
|
|
|
} |
571
|
|
|
|
572
|
|
|
/** |
573
|
|
|
* @param PhoneNumberUtil $util |
574
|
|
|
* @param PhoneNumber $number |
575
|
|
|
* @param string $normalizedCandidate |
576
|
|
|
* @param string[] $formattedNumberGroups |
577
|
|
|
* @return bool |
578
|
|
|
*/ |
579
|
27 |
|
public static function allNumberGroupsAreExactlyPresent( |
580
|
|
|
PhoneNumberUtil $util, |
581
|
|
|
PhoneNumber $number, |
582
|
|
|
$normalizedCandidate, |
583
|
|
|
$formattedNumberGroups |
584
|
|
|
) { |
585
|
27 |
|
$candidateGroups = \preg_split(PhoneNumberUtil::NON_DIGITS_PATTERN, $normalizedCandidate); |
586
|
|
|
|
587
|
|
|
// Set this to the last group, skipping it if the number has an extension. |
588
|
27 |
|
$candidateNumberGroupIndex = $number->hasExtension() ? \count($candidateGroups) - 2 : \count($candidateGroups) - 1; |
589
|
|
|
|
590
|
|
|
// First we check if the national significant number is formatted as a block. |
591
|
|
|
// We use contains and not equals, since the national significant number may be present with |
592
|
|
|
// a prefix such as a national number prefix, or the country code itself. |
593
|
27 |
|
if (\count($candidateGroups) == 1 |
594
|
24 |
|
|| \mb_strpos( |
595
|
24 |
|
$candidateGroups[$candidateNumberGroupIndex], |
596
|
24 |
|
$util->getNationalSignificantNumber($number) |
597
|
27 |
|
) !== false |
598
|
|
|
) { |
599
|
8 |
|
return true; |
600
|
|
|
} |
601
|
|
|
|
602
|
|
|
// Starting from the end, go through in reverse, excluding the first group, and check the |
603
|
|
|
// candidate and number groups are the same. |
604
|
19 |
|
for ($formattedNumberGroupIndex = (\count($formattedNumberGroups) - 1); |
605
|
19 |
|
$formattedNumberGroupIndex > 0 && $candidateNumberGroupIndex >= 0; |
606
|
|
|
$formattedNumberGroupIndex--, $candidateNumberGroupIndex--) { |
607
|
19 |
|
if ($candidateGroups[$candidateNumberGroupIndex] != $formattedNumberGroups[$formattedNumberGroupIndex]) { |
608
|
6 |
|
return false; |
609
|
|
|
} |
610
|
|
|
} |
611
|
|
|
|
612
|
|
|
// Now check the first group. There may be a national prefix at the start, so we only check |
613
|
|
|
// that the candidate group ends with the formatted number group. |
614
|
18 |
|
return ($candidateNumberGroupIndex >= 0 |
615
|
18 |
|
&& \mb_substr( |
616
|
18 |
|
$candidateGroups[$candidateNumberGroupIndex], |
617
|
18 |
|
-\mb_strlen($formattedNumberGroups[0]) |
618
|
18 |
|
) == $formattedNumberGroups[0]); |
619
|
|
|
} |
620
|
|
|
|
621
|
|
|
/** |
622
|
|
|
* Helper method to get the national-number part of a number, formatted without any national |
623
|
|
|
* prefix, and return it as a set of digit blocks that would be formatted together. |
624
|
|
|
* |
625
|
|
|
* @param PhoneNumberUtil $util |
626
|
|
|
* @param PhoneNumber $number |
627
|
|
|
* @param NumberFormat $formattingPattern |
628
|
|
|
* @return string[] |
629
|
|
|
*/ |
630
|
54 |
|
protected static function getNationalNumberGroups( |
631
|
|
|
PhoneNumberUtil $util, |
632
|
|
|
PhoneNumber $number, |
633
|
|
|
NumberFormat $formattingPattern = null |
634
|
|
|
) { |
635
|
54 |
|
if ($formattingPattern === null) { |
636
|
|
|
// This will be in the format +CC-DG;ext=EXT where DG represents groups of digits. |
637
|
54 |
|
$rfc3966Format = $util->format($number, PhoneNumberFormat::RFC3966); |
638
|
|
|
// We remove the extension part from the formatted string before splitting it into different |
639
|
|
|
// groups. |
640
|
54 |
|
$endIndex = \mb_strpos($rfc3966Format, ';'); |
641
|
54 |
|
if ($endIndex === false) { |
642
|
44 |
|
$endIndex = \mb_strlen($rfc3966Format); |
643
|
|
|
} |
644
|
|
|
|
645
|
|
|
// The country-code will have a '-' following it. |
646
|
54 |
|
$startIndex = \mb_strpos($rfc3966Format, '-') + 1; |
647
|
54 |
|
return \explode('-', \mb_substr($rfc3966Format, $startIndex, $endIndex - $startIndex)); |
648
|
|
|
} |
649
|
|
|
|
650
|
|
|
// If a format is provided, we format the NSN only, and split that according to the separator. |
651
|
15 |
|
$nationalSignificantNumber = $util->getNationalSignificantNumber($number); |
652
|
15 |
|
return \explode('-', $util->formatNsnUsingPattern( |
653
|
15 |
|
$nationalSignificantNumber, |
654
|
|
|
$formattingPattern, |
655
|
15 |
|
PhoneNumberFormat::RFC3966 |
656
|
|
|
)); |
657
|
|
|
} |
658
|
|
|
|
659
|
|
|
/** |
660
|
|
|
* @param PhoneNumber $number |
661
|
|
|
* @param string $candidate |
662
|
|
|
* @param PhoneNumberUtil $util |
663
|
|
|
* @param \Closure $checker |
664
|
|
|
* @return bool |
665
|
|
|
*/ |
666
|
54 |
|
public static function checkNumberGroupingIsValid( |
667
|
|
|
PhoneNumber $number, |
668
|
|
|
$candidate, |
669
|
|
|
PhoneNumberUtil $util, |
670
|
|
|
\Closure $checker |
671
|
|
|
) { |
672
|
54 |
|
$normalizedCandidate = PhoneNumberUtil::normalizeDigits($candidate, true /* keep non-digits */); |
673
|
54 |
|
$formattedNumberGroups = static::getNationalNumberGroups($util, $number); |
674
|
54 |
|
if ($checker($util, $number, $normalizedCandidate, $formattedNumberGroups)) { |
675
|
39 |
|
return true; |
676
|
|
|
} |
677
|
|
|
|
678
|
|
|
// If this didn't pass, see if there are any alternative formats that match, and try them instead. |
679
|
15 |
|
$alternateFormats = static::getAlternateFormatsForCountry($number->getCountryCode()); |
680
|
|
|
|
681
|
15 |
|
$nationalSignificantNumber = $util->getNationalSignificantNumber($number); |
682
|
15 |
|
if ($alternateFormats !== null) { |
683
|
15 |
|
foreach ($alternateFormats->numberFormats() as $alternateFormat) { |
684
|
15 |
|
if ($alternateFormat->leadingDigitsPatternSize() > 0) { |
685
|
|
|
// There is only one leading digits pattern for alternate formats. |
686
|
13 |
|
$pattern = $alternateFormat->getLeadingDigitsPattern(0); |
687
|
|
|
|
688
|
13 |
|
$nationalSignificantNumberMatcher = new Matcher($pattern, $nationalSignificantNumber); |
689
|
13 |
|
if (!$nationalSignificantNumberMatcher->lookingAt()) { |
690
|
|
|
// Leading digits don't match; try another one. |
691
|
13 |
|
continue; |
692
|
|
|
} |
693
|
|
|
} |
694
|
|
|
|
695
|
15 |
|
$formattedNumberGroups = static::getNationalNumberGroups($util, $number, $alternateFormat); |
696
|
15 |
|
if ($checker($util, $number, $normalizedCandidate, $formattedNumberGroups)) { |
697
|
11 |
|
return true; |
698
|
|
|
} |
699
|
|
|
} |
700
|
|
|
} |
701
|
4 |
|
return false; |
702
|
|
|
} |
703
|
|
|
|
704
|
|
|
/** |
705
|
|
|
* @param PhoneNumber $number |
706
|
|
|
* @param string $candidate |
707
|
|
|
* @return bool |
708
|
|
|
*/ |
709
|
55 |
|
public static function containsMoreThanOneSlashInNationalNumber(PhoneNumber $number, $candidate) |
710
|
|
|
{ |
711
|
55 |
|
$firstSlashInBodyIndex = \mb_strpos($candidate, '/'); |
712
|
55 |
|
if ($firstSlashInBodyIndex === false) { |
713
|
|
|
// No slashes, this is okay |
714
|
53 |
|
return false; |
715
|
|
|
} |
716
|
|
|
|
717
|
|
|
// Now look for a second one. |
718
|
2 |
|
$secondSlashInBodyIndex = \mb_strpos($candidate, '/', $firstSlashInBodyIndex + 1); |
719
|
2 |
|
if ($secondSlashInBodyIndex === false) { |
720
|
|
|
// Only one slash, this is okay |
721
|
1 |
|
return false; |
722
|
|
|
} |
723
|
|
|
|
724
|
|
|
// If the first slash is after the country calling code, this is permitted |
725
|
1 |
|
$candidateHasCountryCode = ($number->getCountryCodeSource() === CountryCodeSource::FROM_NUMBER_WITH_PLUS_SIGN |
726
|
1 |
|
|| $number->getCountryCodeSource() === CountryCodeSource::FROM_NUMBER_WITHOUT_PLUS_SIGN); |
727
|
|
|
|
728
|
1 |
|
if ($candidateHasCountryCode |
729
|
1 |
|
&& PhoneNumberUtil::normalizeDigitsOnly( |
730
|
1 |
|
\mb_substr($candidate, 0, $firstSlashInBodyIndex) |
731
|
1 |
|
) == $number->getCountryCode() |
732
|
|
|
) { |
733
|
|
|
// Any more slashes and this is illegal |
734
|
1 |
|
return (\mb_strpos(\mb_substr($candidate, $secondSlashInBodyIndex + 1), '/') !== false); |
735
|
|
|
} |
736
|
|
|
|
737
|
1 |
|
return true; |
738
|
|
|
} |
739
|
|
|
|
740
|
|
|
/** |
741
|
|
|
* @param PhoneNumber $number |
742
|
|
|
* @param string $candidate |
743
|
|
|
* @param PhoneNumberUtil $util |
744
|
|
|
* @return bool |
745
|
|
|
*/ |
746
|
99 |
|
public static function containsOnlyValidXChars(PhoneNumber $number, $candidate, PhoneNumberUtil $util) |
747
|
|
|
{ |
748
|
|
|
// The characters 'x' and 'X' can be (1) a carrier code, in which case they always precede the |
749
|
|
|
// national significant number or (2) an extension sign, in which case they always precede the |
750
|
|
|
// extension number. We assume a carrier code is more than 1 digit, so the first case has to |
751
|
|
|
// have more than 1 consecutive 'x' or 'X', whereas the second case can only have exactly 1 'x' |
752
|
|
|
// or 'X'. We ignore the character if it appears as the last character of the string. |
753
|
99 |
|
$candidateLength = \mb_strlen($candidate); |
754
|
|
|
|
755
|
99 |
|
for ($index = 0; $index < $candidateLength - 1; $index++) { |
756
|
99 |
|
$charAtIndex = \mb_substr($candidate, $index, 1); |
757
|
99 |
|
if ($charAtIndex == 'x' || $charAtIndex == 'X') { |
758
|
15 |
|
$charAtNextIndex = \mb_substr($candidate, $index + 1, 1); |
759
|
15 |
|
if ($charAtNextIndex == 'x' || $charAtNextIndex == 'X') { |
760
|
|
|
// This is the carrier code case, in which the 'X's always precede the national |
761
|
|
|
// significant number. |
762
|
|
|
$index++; |
763
|
|
|
|
764
|
|
|
if ($util->isNumberMatch($number, \mb_substr($candidate, $index)) != MatchType::NSN_MATCH) { |
765
|
|
|
return false; |
766
|
|
|
} |
767
|
15 |
|
} elseif (!PhoneNumberUtil::normalizeDigitsOnly(\mb_substr( |
768
|
15 |
|
$candidate, |
769
|
15 |
|
$index |
770
|
15 |
|
)) == $number->getExtension() |
771
|
|
|
) { |
772
|
|
|
// This is the extension sign case, in which the 'x' or 'X' should always precede the |
773
|
|
|
// extension number |
774
|
|
|
return false; |
775
|
|
|
} |
776
|
|
|
} |
777
|
|
|
} |
778
|
99 |
|
return true; |
779
|
|
|
} |
780
|
|
|
|
781
|
|
|
/** |
782
|
|
|
* @param PhoneNumber $number |
783
|
|
|
* @param PhoneNumberUtil $util |
784
|
|
|
* @return bool |
785
|
|
|
*/ |
786
|
99 |
|
public static function isNationalPrefixPresentIfRequired(PhoneNumber $number, PhoneNumberUtil $util) |
787
|
|
|
{ |
788
|
|
|
// First, check how we deduced the country code. If it was written in international format, then |
789
|
|
|
// the national prefix is not required. |
790
|
99 |
|
if ($number->getCountryCodeSource() !== CountryCodeSource::FROM_DEFAULT_COUNTRY) { |
791
|
41 |
|
return true; |
792
|
|
|
} |
793
|
|
|
|
794
|
65 |
|
$phoneNumberRegion = $util->getRegionCodeForCountryCode($number->getCountryCode()); |
795
|
65 |
|
$metadata = $util->getMetadataForRegion($phoneNumberRegion); |
796
|
65 |
|
if ($metadata === null) { |
797
|
|
|
return true; |
798
|
|
|
} |
799
|
|
|
|
800
|
|
|
// Check if a national prefix should be present when formatting this number. |
801
|
65 |
|
$nationalNumber = $util->getNationalSignificantNumber($number); |
802
|
65 |
|
$formatRule = $util->chooseFormattingPatternForNumber($metadata->numberFormats(), $nationalNumber); |
803
|
|
|
// To do this, we check that a national prefix formatting rule was present and that it wasn't |
804
|
|
|
// just the first-group symbol ($1) with punctuation. |
805
|
65 |
|
if (($formatRule !== null) && $formatRule->getNationalPrefixFormattingRule() !== '') { |
806
|
44 |
|
if ($formatRule->getNationalPrefixOptionalWhenFormatting()) { |
807
|
|
|
// The national-prefix is optional in these cases, so we don't need to check if it was |
808
|
|
|
// present. |
809
|
7 |
|
return true; |
810
|
|
|
} |
811
|
|
|
|
812
|
37 |
|
if (PhoneNumberUtil::formattingRuleHasFirstGroupOnly($formatRule->getNationalPrefixFormattingRule())) { |
813
|
|
|
// National Prefix not needed for this number. |
814
|
3 |
|
return true; |
815
|
|
|
} |
816
|
|
|
|
817
|
|
|
// Normalize the remainder. |
818
|
34 |
|
$rawInputCopy = PhoneNumberUtil::normalizeDigitsOnly($number->getRawInput()); |
819
|
34 |
|
$rawInput = $rawInputCopy; |
820
|
|
|
// Check if we found a national prefix and/or carrier code at the start of the raw input, and |
821
|
|
|
// return the result. |
822
|
34 |
|
$carrierCode = null; |
823
|
34 |
|
return $util->maybeStripNationalPrefixAndCarrierCode($rawInput, $metadata, $carrierCode); |
824
|
|
|
} |
825
|
25 |
|
return true; |
826
|
|
|
} |
827
|
|
|
|
828
|
|
|
|
829
|
|
|
/** |
830
|
|
|
* Storage for Alternate Formats |
831
|
|
|
* @var PhoneMetadata[] |
832
|
|
|
*/ |
833
|
|
|
protected static $callingCodeToAlternateFormatsMap = array(); |
834
|
|
|
|
835
|
|
|
/** |
836
|
|
|
* @param $countryCallingCode |
837
|
|
|
* @return PhoneMetadata|null |
838
|
|
|
*/ |
839
|
15 |
|
protected static function getAlternateFormatsForCountry($countryCallingCode) |
840
|
|
|
{ |
841
|
15 |
|
$countryCodeSet = AlternateFormatsCountryCodeSet::$alternateFormatsCountryCodeSet; |
842
|
|
|
|
843
|
15 |
|
if (!\in_array($countryCallingCode, $countryCodeSet)) { |
844
|
|
|
return null; |
845
|
|
|
} |
846
|
|
|
|
847
|
15 |
|
if (!isset(static::$callingCodeToAlternateFormatsMap[$countryCallingCode])) { |
848
|
3 |
|
static::loadAlternateFormatsMetadataFromFile($countryCallingCode); |
849
|
|
|
} |
850
|
|
|
|
851
|
15 |
|
return static::$callingCodeToAlternateFormatsMap[$countryCallingCode]; |
852
|
|
|
} |
853
|
|
|
|
854
|
|
|
/** |
855
|
|
|
* @param string $countryCallingCode |
856
|
|
|
* @throws \Exception |
857
|
|
|
*/ |
858
|
3 |
|
protected static function loadAlternateFormatsMetadataFromFile($countryCallingCode) |
859
|
|
|
{ |
860
|
3 |
|
$fileName = static::$alternateFormatsFilePrefix . '_' . $countryCallingCode . '.php'; |
861
|
|
|
|
862
|
3 |
|
if (!\is_readable($fileName)) { |
863
|
|
|
throw new \Exception('missing metadata: ' . $fileName); |
864
|
|
|
} |
865
|
|
|
|
866
|
3 |
|
$metadataLoader = new DefaultMetadataLoader(); |
867
|
3 |
|
$data = $metadataLoader->loadMetadata($fileName); |
868
|
3 |
|
$metadata = new PhoneMetadata(); |
869
|
3 |
|
$metadata->fromArray($data); |
870
|
3 |
|
static::$callingCodeToAlternateFormatsMap[$countryCallingCode] = $metadata; |
871
|
3 |
|
} |
872
|
|
|
|
873
|
|
|
|
874
|
|
|
/** |
875
|
|
|
* Return the current element |
876
|
|
|
* @link http://php.net/manual/en/iterator.current.php |
877
|
|
|
* @return PhoneNumberMatch|null |
878
|
|
|
*/ |
879
|
|
|
#[\ReturnTypeWillChange] |
880
|
199 |
|
public function current() |
881
|
|
|
{ |
882
|
199 |
|
return $this->lastMatch; |
883
|
|
|
} |
884
|
|
|
|
885
|
|
|
/** |
886
|
|
|
* Move forward to next element |
887
|
|
|
* @link http://php.net/manual/en/iterator.next.php |
888
|
|
|
* @return void Any returned value is ignored. |
889
|
|
|
*/ |
890
|
|
|
#[\ReturnTypeWillChange] |
891
|
201 |
|
public function next() |
892
|
|
|
{ |
893
|
201 |
|
$this->lastMatch = $this->find($this->searchIndex); |
|
|
|
|
894
|
|
|
|
895
|
201 |
|
if ($this->lastMatch === null) { |
896
|
95 |
|
$this->state = 'DONE'; |
897
|
|
|
} else { |
898
|
126 |
|
$this->searchIndex = $this->lastMatch->end(); |
|
|
|
|
899
|
126 |
|
$this->state = 'READY'; |
900
|
|
|
} |
901
|
|
|
|
902
|
201 |
|
$this->searchIndex++; |
903
|
201 |
|
} |
904
|
|
|
|
905
|
|
|
/** |
906
|
|
|
* Return the key of the current element |
907
|
|
|
* @link http://php.net/manual/en/iterator.key.php |
908
|
|
|
* @return mixed scalar on success, or null on failure. |
909
|
|
|
* @since 5.0.0 |
910
|
|
|
*/ |
911
|
|
|
#[\ReturnTypeWillChange] |
912
|
|
|
public function key() |
913
|
|
|
{ |
914
|
|
|
return $this->searchIndex; |
915
|
|
|
} |
916
|
|
|
|
917
|
|
|
/** |
918
|
|
|
* Checks if current position is valid |
919
|
|
|
* @link http://php.net/manual/en/iterator.valid.php |
920
|
|
|
* @return boolean The return value will be casted to boolean and then evaluated. |
921
|
|
|
* Returns true on success or false on failure. |
922
|
|
|
* @since 5.0.0 |
923
|
|
|
*/ |
924
|
|
|
#[\ReturnTypeWillChange] |
925
|
29 |
|
public function valid() |
926
|
|
|
{ |
927
|
29 |
|
return $this->state === 'READY'; |
928
|
|
|
} |
929
|
|
|
|
930
|
|
|
/** |
931
|
|
|
* Rewind the Iterator to the first element |
932
|
|
|
* @link http://php.net/manual/en/iterator.rewind.php |
933
|
|
|
* @return void Any returned value is ignored. |
934
|
|
|
* @since 5.0.0 |
935
|
|
|
*/ |
936
|
|
|
#[\ReturnTypeWillChange] |
937
|
18 |
|
public function rewind() |
938
|
|
|
{ |
939
|
18 |
|
$this->searchIndex = 0; |
940
|
18 |
|
$this->next(); |
941
|
18 |
|
} |
942
|
|
|
} |
943
|
|
|
|