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