Passed
Push — upstream-8.11.0 ( 4431ea )
by Joshua
03:25
created

PhoneNumberMatcher::allNumberGroupsRemainGrouped()   B

Complexity

Conditions 9
Paths 16

Size

Total Lines 56
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 9.5145

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 22
c 1
b 0
f 0
nc 16
nop 4
dl 0
loc 56
ccs 22
cts 27
cp 0.8148
crap 9.5145
rs 8.0555

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 1
            '/+(.*)',
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 1
            "(\\([^(]*)",
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 1
            "(?:\\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 1
            "[‒-―-]\\p{Z}*(.+)",
121
            // Breaks on a full stop - e.g. "12345. 332-445-1234 is my number."
122 1
            "\\.+\\p{Z}*([^.]+)",
123
            // Breaks on space - e.g. "3324451234 8002341234"
124
            "\\p{Z}+(\\P{Z}+)"
125 1
        );
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::initCapturingExtnDigits();
182 1
        PhoneNumberUtil::initExtnPatterns();
183
184
185
        // Phone number pattern allowing optional punctuation.
186 1
        static::$pattern = '(?:' . $leadClass . $punctuation . ')' . $leadLimit
187 1
            . $digitSequence . '(?:' . $punctuation . $digitSequence . ')' . $blockLimit
188 1
            . '(?:' . PhoneNumberUtil::$EXTN_PATTERNS_FOR_MATCHING . ')?';
189
190 1
        static::$initialized = true;
191 1
    }
192
193
    /**
194
     * Helper function to generate regular expression with an upper and lower limit.
195
     *
196
     * @param int $lower
197
     * @param int $upper
198
     * @return string
199
     */
200 1
    protected static function limit($lower, $upper)
201
    {
202 1
        if (($lower < 0) || ($upper <= 0) || ($upper < $lower)) {
203
            throw new \InvalidArgumentException();
204
        }
205
206 1
        return '{' . $lower . ',' . $upper . '}';
207
    }
208
209
    /**
210
     * The phone number utility.
211
     * @var PhoneNumberUtil
212
     */
213
    protected $phoneUtil;
214
215
    /**
216
     * The text searched for phone numbers.
217
     * @var string
218
     */
219
    protected $text;
220
221
    /**
222
     * The region (country) to assume for phone numbers without an international prefix, possibly
223
     * null.
224
     * @var string
225
     */
226
    protected $preferredRegion;
227
228
    /**
229
     * The degrees of validation requested.
230
     * @var AbstractLeniency
231
     */
232
    protected $leniency;
233
234
    /**
235
     * The maximum number of retires after matching an invalid number.
236
     * @var int
237
     */
238
    protected $maxTries;
239
240
    /**
241
     * One of:
242
     *  - NOT_READY
243
     *  - READY
244
     *  - DONE
245
     * @var string
246
     */
247
    protected $state = 'NOT_READY';
248
249
    /**
250
     * The last successful match, null unless $this->state = READY
251
     * @var PhoneNumberMatch
252
     */
253
    protected $lastMatch;
254
255
    /**
256
     * The next index to start searching at. Undefined when $this->state = DONE
257
     * @var int
258
     */
259
    protected $searchIndex = 0;
260
261
    /**
262
     * Creates a new instance. See the factory methods in PhoneNumberUtil on how to obtain a new instance.
263
     *
264
     *
265
     * @param PhoneNumberUtil $util The Phone Number Util to use
266
     * @param string|null $text The text that we will search, null for no text
267
     * @param string|null $country The country to assume for phone numbers not written in international format.
268
     *  (with a leading plus, or with the international dialling prefix of the specified region).
269
     *  May be null, or "ZZ" if only numbers with a leading plus should be considered.
270
     * @param AbstractLeniency $leniency The leniency to use when evaluating candidate phone numbers
271
     * @param int $maxTries The maximum number of invalid numbers to try before giving up on the text.
272
     *  This is to cover degenerate cases where the text has a lot of false positives in it. Must be >= 0
273
     * @throws \NullPointerException
274
     * @throws \InvalidArgumentException
275
     */
276 207
    public function __construct(PhoneNumberUtil $util, $text, $country, AbstractLeniency $leniency, $maxTries)
277
    {
278 207
        if ($maxTries < 0) {
279
            throw new \InvalidArgumentException();
280
        }
281
282 207
        $this->phoneUtil = $util;
283 207
        $this->text = ($text !== null) ? $text : '';
284 207
        $this->preferredRegion = $country;
285 207
        $this->leniency = $leniency;
286 207
        $this->maxTries = $maxTries;
287
288 207
        if (static::$initialized === false) {
289 1
            static::init();
290 1
        }
291 207
    }
292
293
    /**
294
     * Attempts to find the next subsequence in the searched sequence on or after {@code searchIndex}
295
     * that represents a phone number. Returns the next match, null if none was found.
296
     *
297
     * @param int $index The search index to start searching at
298
     * @return PhoneNumberMatch|null The Phone Number Match found, null if none can be found
299
     */
300 201
    protected function find($index)
301
    {
302 201
        $matcher = new Matcher(static::$pattern, $this->text);
303 201
        while (($this->maxTries > 0) && $matcher->find($index)) {
304 200
            $start = $matcher->start();
305 200
            $cutLength = $matcher->end() - $start;
306 200
            $candidate = \mb_substr($this->text, $start, $cutLength);
307
308
            // Check for extra numbers at the end.
309
            // TODO: This is the place to start when trying to support extraction of multiple phone number
310
            // from split notations (+41 49 123 45 67 / 68).
311 200
            $candidate = static::trimAfterFirstMatch(PhoneNumberUtil::$SECOND_NUMBER_START_PATTERN, $candidate);
312
313 200
            $match = $this->extractMatch($candidate, $start);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $match is correct as $this->extractMatch($candidate, $start) targeting libphonenumber\PhoneNumberMatcher::extractMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
314 200
            if ($match !== null) {
315 126
                return $match;
316
            }
317
318 92
            $index = $start + \mb_strlen($candidate);
319 92
            $this->maxTries--;
320 92
        }
321
322 95
        return null;
323
    }
324
325
    /**
326
     * Trims away any characters after the first match of $pattern in $candidate,
327
     * returning the trimmed version.
328
     *
329
     * @param string $pattern
330
     * @param string $candidate
331
     * @return string
332
     */
333 200
    protected static function trimAfterFirstMatch($pattern, $candidate)
334
    {
335 200
        $trailingCharsMatcher = new Matcher($pattern, $candidate);
336 200
        if ($trailingCharsMatcher->find()) {
337 10
            $startChar = $trailingCharsMatcher->start();
338 10
            $candidate = \mb_substr($candidate, 0, $startChar);
339 10
        }
340 200
        return $candidate;
341
    }
342
343
    /**
344
     * Helper method to determine if a character is a Latin-script letter or not. For our purposes,
345
     * combining marks should also return true since we assume they have been added to a preceding
346
     * Latin character.
347
     *
348
     * @param string $letter
349
     * @return bool
350
     * @internal
351
     */
352 60
    public static function isLatinLetter($letter)
353
    {
354
        // Combining marks are a subset of non-spacing-mark.
355 60
        if (\preg_match('/\p{L}/u', $letter) !== 1 && \preg_match('/\p{Mn}/u', $letter) !== 1) {
356 54
            return false;
357
        }
358
359 9
        return (\preg_match('/\p{Latin}/u', $letter) === 1)
360 9
        || (\preg_match('/\pM+/u', $letter) === 1);
361
    }
362
363
    /**
364
     * @param string $character
365
     * @return bool
366
     */
367 49
    protected static function isInvalidPunctuationSymbol($character)
368
    {
369 49
        return $character == '%' || \preg_match('/\p{Sc}/u', $character);
370
    }
371
372
    /**
373
     * Attempts to extract a match from a $candidate.
374
     *
375
     * @param string $candidate The candidate text that might contain a phone number
376
     * @param int $offset The offset of $candidate within $this->text
377
     * @return PhoneNumberMatch|null The match found, null if none can be found
378
     */
379 200
    protected function extractMatch($candidate, $offset)
380
    {
381
        // Skip a match that is more likely to be a date.
382 200
        $dateMatcher = new Matcher(static::$slashSeparatedDates, $candidate);
383 200
        if ($dateMatcher->find()) {
384 33
            return null;
385
        }
386
387
        // Skip potential time-stamps.
388 180
        $timeStampMatcher = new Matcher(static::$timeStamps, $candidate);
389 180
        if ($timeStampMatcher->find()) {
390 20
            $followingText = \mb_substr($this->text, $offset + \mb_strlen($candidate));
391 20
            $timeStampSuffixMatcher = new Matcher(static::$timeStampsSuffix, $followingText);
392 20
            if ($timeStampSuffixMatcher->lookingAt()) {
393 16
                return null;
394
            }
395 4
        }
396
397
        // Try to come up with a valid match given the entire candidate.
398 180
        $match = $this->parseAndVerify($candidate, $offset);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $match is correct as $this->parseAndVerify($candidate, $offset) targeting libphonenumber\PhoneNumb...tcher::parseAndVerify() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
399 180
        if ($match !== null) {
0 ignored issues
show
introduced by
The condition $match !== null is always false.
Loading history...
400 124
            return $match;
401
        }
402
403
        // If that failed, try to find an "inner match" - there might be a phone number within this
404
        // candidate.
405 76
        return $this->extractInnerMatch($candidate, $offset);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->extractInnerMatch($candidate, $offset) targeting libphonenumber\PhoneNumb...er::extractInnerMatch() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
406
    }
407
408
    /**
409
     * Attempts to extract a match from $candidate if the whole candidate does not qualify as a
410
     * match.
411
     *
412
     * @param string $candidate The candidate text that might contact a phone number
413
     * @param int $offset The current offset of $candidate within $this->text
414
     * @return PhoneNumberMatch|null The match found, null if none can be found
415
     */
416 76
    protected function extractInnerMatch($candidate, $offset)
417
    {
418 76
        foreach (static::$innerMatches as $possibleInnerMatch) {
419 76
            $groupMatcher = new Matcher($possibleInnerMatch, $candidate);
420 76
            $isFirstMatch = true;
421
422 76
            while ($groupMatcher->find() && $this->maxTries > 0) {
423 20
                if ($isFirstMatch) {
424
                    // We should handle any group before this one too.
425 20
                    $group = static::trimAfterFirstMatch(PhoneNumberUtil::$UNWANTED_END_CHAR_PATTERN,
426 20
                        \mb_substr($candidate, 0, $groupMatcher->start()));
427
428 20
                    $match = $this->parseAndVerify($group, $offset);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $match is correct as $this->parseAndVerify($group, $offset) targeting libphonenumber\PhoneNumb...tcher::parseAndVerify() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
429 20
                    if ($match !== null) {
430 6
                        return $match;
431
                    }
432 17
                    $this->maxTries--;
433 17
                    $isFirstMatch = false;
434 17
                }
435 17
                $group = static::trimAfterFirstMatch(PhoneNumberUtil::$UNWANTED_END_CHAR_PATTERN,
436 17
                    $groupMatcher->group(1));
437 17
                $match = $this->parseAndVerify($group, $offset + $groupMatcher->start(1));
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $match is correct as $this->parseAndVerify($g...groupMatcher->start(1)) targeting libphonenumber\PhoneNumb...tcher::parseAndVerify() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
438 17
                if ($match !== null) {
439 7
                    return $match;
440
                }
441 16
                $this->maxTries--;
442 16
            }
443 76
        }
444 72
        return null;
445
    }
446
447
    /**
448
     * Parses a phone number from the $candidate} using PhoneNumberUtil::parse() and
449
     * verifies it matches the requested leniency. If parsing and verification succeed, a
450
     * corresponding PhoneNumberMatch is returned, otherwise this method returns null.
451
     *
452
     * @param string $candidate The candidate match
453
     * @param int $offset The offset of $candidate within $this->text
454
     * @return PhoneNumberMatch|null The parsed and validated phone number match, or null
455
     */
456 180
    protected function parseAndVerify($candidate, $offset)
457
    {
458
        try {
459
            // Check the candidate doesn't contain any formatting which would indicate that it really
460
            // isn't a phone number
461 180
            $matchingBracketsMatcher = new Matcher(static::$matchingBrackets, $candidate);
462 180
            $pubPagesMatcher = new Matcher(static::$pubPages, $candidate);
463 180
            if (!$matchingBracketsMatcher->matches() || $pubPagesMatcher->find()) {
464 11
                return null;
465
            }
466
467
            // If leniency is set to VALID or stricter, we also want to skip numbers that are surrounded
468
            // by Latin alphabetic characters, to skip cases like abc8005001234 or 8005001234def.
469 180
            if ($this->leniency->compareTo(Leniency::VALID()) >= 0) {
470
                // If the candidate is not at the start of the text, and does not start with phone-number
471
                // punctuation, check the previous character.
472 137
                $leadClassMatcher = new Matcher(static::$leadClass, $candidate);
473 137
                if ($offset > 0 && !$leadClassMatcher->lookingAt()) {
474 44
                    $previousChar = \mb_substr($this->text, $offset - 1, 1);
475
                    // We return null if it is a latin letter or an invalid punctuation symbol.
476 44
                    if (static::isInvalidPunctuationSymbol($previousChar) || static::isLatinLetter($previousChar)) {
477 2
                        return null;
478
                    }
479 44
                }
480 137
                $lastCharIndex = $offset + \mb_strlen($candidate);
481 137
                if ($lastCharIndex < \mb_strlen($this->text)) {
482 40
                    $nextChar = \mb_substr($this->text, $lastCharIndex, 1);
483 40
                    if (static::isInvalidPunctuationSymbol($nextChar) || static::isLatinLetter($nextChar)) {
484 2
                        return null;
485
                    }
486 39
                }
487 136
            }
488
489 179
            $number = $this->phoneUtil->parseAndKeepRawInput($candidate, $this->preferredRegion);
490
491 178
            if ($this->leniency->verify($number, $candidate, $this->phoneUtil)) {
492
                // We used parseAndKeepRawInput to create this number, but for now we don't return the extra
493
                // values parsed. TODO: stop clearing all values here and switch all users over
494
                // to using rawInput() rather than the rawString() of PhoneNumberMatch
495 126
                $number->clearCountryCodeSource();
496 126
                $number->clearRawInput();
497 126
                $number->clearPreferredDomesticCarrierCode();
498 126
                return new PhoneNumberMatch($offset, $candidate, $number);
499
            }
500 74
        } catch (NumberParseException $e) {
501
            // ignore and continue
502
        }
503 74
        return null;
504
    }
505
506
    /**
507
     * @param PhoneNumberUtil $util
508
     * @param PhoneNumber $number
509
     * @param string $normalizedCandidate
510
     * @param string[] $formattedNumberGroups
511
     * @return bool
512
     */
513 27
    public static function allNumberGroupsRemainGrouped(
514
        PhoneNumberUtil $util,
515
        PhoneNumber $number,
516
        $normalizedCandidate,
517
        $formattedNumberGroups
518
    ) {
519 27
        $fromIndex = 0;
520 27
        if ($number->getCountryCodeSource() !== CountryCodeSource::FROM_DEFAULT_COUNTRY) {
521
            // First skip the country code if the normalized candidate contained it.
522 11
            $countryCode = $number->getCountryCode();
523 11
            $fromIndex = \mb_strpos($normalizedCandidate, $countryCode) + \mb_strlen($countryCode);
524 11
        }
525
526
        // Check each group of consecutive digits are not broken into separate groupings in the
527
        // $normalizedCandidate string.
528 27
        $formattedNumberGroupsLength = \count($formattedNumberGroups);
529 27
        for ($i = 0; $i < $formattedNumberGroupsLength; $i++) {
530
            // Fails if the substring of $normalizedCandidate starting from $fromIndex
531
            // doesn't contain the consecutive digits in $formattedNumberGroups[$i].
532 27
            $fromIndex = \mb_strpos($normalizedCandidate, $formattedNumberGroups[$i], $fromIndex);
533 27
            if ($fromIndex === false) {
534 9
                return false;
535
            }
536
537
            // Moves $fromIndex forward.
538 26
            $fromIndex += \mb_strlen($formattedNumberGroups[$i]);
539 26
            if ($i === 0 && $fromIndex < \mb_strlen($normalizedCandidate)) {
540
                // We are at the position right after the NDC. We get the region used for formatting
541
                // information based on the country code in the phone number, rather than the number itself,
542
                // as we do not need to distinguish between different countries with the same country
543
                // calling code and this is faster.
544 26
                $region = $util->getRegionCodeForCountryCode($number->getCountryCode());
545
546 26
                if ($util->getNddPrefixForRegion($region, true) !== null
547 26
                    && \is_int(\mb_substr($normalizedCandidate, $fromIndex, 1))
548 26
                ) {
549
                    // This means there is no formatting symbol after the NDC. In this case, we only
550
                    // accept the number if there is no formatting symbol at all in the number, except
551
                    // for extensions. This is only important for countries with national prefixes.
552
                    $nationalSignificantNumber = $util->getNationalSignificantNumber($number);
553
                    return \mb_substr(
554
                        \mb_substr($normalizedCandidate, $fromIndex - \mb_strlen($formattedNumberGroups[$i])),
555
                        \mb_strlen($nationalSignificantNumber)
556
                    ) === $nationalSignificantNumber;
557
                }
558 26
            }
559 26
        }
560
        // The check here makes sure that we haven't mistakenly already used the extension to
561
        // match the last group of the subscriber number. Note the extension cannot have
562
        // formatting in-between digits
563
564 25
        if ($number->hasExtension()) {
565 4
            return \mb_strpos(\mb_substr($normalizedCandidate, $fromIndex), $number->getExtension()) !== false;
566
        }
567
568 21
        return true;
569
    }
570
571
    /**
572
     * @param PhoneNumberUtil $util
573
     * @param PhoneNumber $number
574
     * @param string $normalizedCandidate
575
     * @param string[] $formattedNumberGroups
576
     * @return bool
577
     */
578 27
    public static function allNumberGroupsAreExactlyPresent(
579
        PhoneNumberUtil $util,
580
        PhoneNumber $number,
581
        $normalizedCandidate,
582
        $formattedNumberGroups
583
    ) {
584 27
        $candidateGroups = \preg_split(PhoneNumberUtil::NON_DIGITS_PATTERN, $normalizedCandidate);
585
586
        // Set this to the last group, skipping it if the number has an extension.
587 27
        $candidateNumberGroupIndex = $number->hasExtension() ? \count($candidateGroups) - 2 : \count($candidateGroups) - 1;
0 ignored issues
show
Bug introduced by
It seems like $candidateGroups can also be of type false; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

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

587
        $candidateNumberGroupIndex = $number->hasExtension() ? \count(/** @scrutinizer ignore-type */ $candidateGroups) - 2 : \count($candidateGroups) - 1;
Loading history...
588
589
        // First we check if the national significant number is formatted as a block.
590
        // We use contains and not equals, since the national significant number may be present with
591
        // a prefix such as a national number prefix, or the country code itself.
592 27
        if (\count($candidateGroups) == 1
593 27
            || \mb_strpos($candidateGroups[$candidateNumberGroupIndex],
594 24
                $util->getNationalSignificantNumber($number)) !== false
595 27
        ) {
596 8
            return true;
597
        }
598
599
        // Starting from the end, go through in reverse, excluding the first group, and check the
600
        // candidate and number groups are the same.
601 19
        for ($formattedNumberGroupIndex = (\count($formattedNumberGroups) - 1);
602 19
             $formattedNumberGroupIndex > 0 && $candidateNumberGroupIndex >= 0;
603 19
             $formattedNumberGroupIndex--, $candidateNumberGroupIndex--) {
604 19
            if ($candidateGroups[$candidateNumberGroupIndex] != $formattedNumberGroups[$formattedNumberGroupIndex]) {
605 6
                return false;
606
            }
607 19
        }
608
609
        // Now check the first group. There may be a national prefix at the start, so we only check
610
        // that the candidate group ends with the formatted number group.
611
        return ($candidateNumberGroupIndex >= 0
612 18
            && \mb_substr($candidateGroups[$candidateNumberGroupIndex],
613 18
                -\mb_strlen($formattedNumberGroups[0])) == $formattedNumberGroups[0]);
614
    }
615
616
    /**
617
     * Helper method to get the national-number part of a number, formatted without any national
618
     * prefix, and return it as a set of digit blocks that would be formatted together.
619
     *
620
     * @param PhoneNumberUtil $util
621
     * @param PhoneNumber $number
622
     * @param NumberFormat $formattingPattern
623
     * @return string[]
624
     */
625 54
    protected static function getNationalNumberGroups(
626
        PhoneNumberUtil $util,
627
        PhoneNumber $number,
628
        NumberFormat $formattingPattern = null
629
    ) {
630 54
        if ($formattingPattern === null) {
631
            // This will be in the format +CC-DG;ext=EXT where DG represents groups of digits.
632 54
            $rfc3966Format = $util->format($number, PhoneNumberFormat::RFC3966);
633
            // We remove the extension part from the formatted string before splitting it into different
634
            // groups.
635 54
            $endIndex = \mb_strpos($rfc3966Format, ';');
636 54
            if ($endIndex === false) {
637 44
                $endIndex = \mb_strlen($rfc3966Format);
638 44
            }
639
640
            // The country-code will have a '-' following it.
641 54
            $startIndex = \mb_strpos($rfc3966Format, '-') + 1;
642 54
            return \explode('-', \mb_substr($rfc3966Format, $startIndex, $endIndex - $startIndex));
643
        }
644
645
        // If a format is provided, we format the NSN only, and split that according to the separator.
646 15
        $nationalSignificantNumber = $util->getNationalSignificantNumber($number);
647 15
        return \explode('-', $util->formatNsnUsingPattern($nationalSignificantNumber, $formattingPattern,
648 15
            PhoneNumberFormat::RFC3966));
649
    }
650
651
    /**
652
     * @param PhoneNumber $number
653
     * @param string $candidate
654
     * @param PhoneNumberUtil $util
655
     * @param \Closure $checker
656
     * @return bool
657
     */
658 54
    public static function checkNumberGroupingIsValid(
659
        PhoneNumber $number,
660
        $candidate,
661
        PhoneNumberUtil $util,
662
        \Closure $checker
663
    ) {
664 54
        $normalizedCandidate = PhoneNumberUtil::normalizeDigits($candidate, true /* keep non-digits */);
665 54
        $formattedNumberGroups = static::getNationalNumberGroups($util, $number);
666 54
        if ($checker($util, $number, $normalizedCandidate, $formattedNumberGroups)) {
667 39
            return true;
668
        }
669
670
        // If this didn't pass, see if there are any alternative formats that match, and try them instead.
671 15
        $alternateFormats = static::getAlternateFormatsForCountry($number->getCountryCode());
672
673 15
        $nationalSignificantNumber = $util->getNationalSignificantNumber($number);
674 15
        if ($alternateFormats !== null) {
675 15
            foreach ($alternateFormats->numberFormats() as $alternateFormat) {
676 15
                if ($alternateFormat->leadingDigitsPatternSize() > 0) {
677
                    // There is only one leading digits pattern for alternate formats.
678 13
                    $pattern = $alternateFormat->getLeadingDigitsPattern(0);
679
680 13
                    $nationalSignificantNumberMatcher = new Matcher($pattern, $nationalSignificantNumber);
681 13
                    if (!$nationalSignificantNumberMatcher->lookingAt()) {
682
                        // Leading digits don't match; try another one.
683 13
                        continue;
684
                    }
685 13
                }
686
687 15
                $formattedNumberGroups = static::getNationalNumberGroups($util, $number, $alternateFormat);
688 15
                if ($checker($util, $number, $normalizedCandidate, $formattedNumberGroups)) {
689 11
                    return true;
690
                }
691 4
            }
692 4
        }
693 4
        return false;
694
    }
695
696
    /**
697
     * @param PhoneNumber $number
698
     * @param string $candidate
699
     * @return bool
700
     */
701 55
    public static function containsMoreThanOneSlashInNationalNumber(PhoneNumber $number, $candidate)
702
    {
703 55
        $firstSlashInBodyIndex = \mb_strpos($candidate, '/');
704 55
        if ($firstSlashInBodyIndex === false) {
705
            // No slashes, this is okay
706 53
            return false;
707
        }
708
709
        // Now look for a second one.
710 2
        $secondSlashInBodyIndex = \mb_strpos($candidate, '/', $firstSlashInBodyIndex + 1);
711 2
        if ($secondSlashInBodyIndex === false) {
712
            // Only one slash, this is okay
713 1
            return false;
714
        }
715
716
        // If the first slash is after the country calling code, this is permitted
717 1
        $candidateHasCountryCode = ($number->getCountryCodeSource() === CountryCodeSource::FROM_NUMBER_WITH_PLUS_SIGN
718 1
            || $number->getCountryCodeSource() === CountryCodeSource::FROM_NUMBER_WITHOUT_PLUS_SIGN);
719
720
        if ($candidateHasCountryCode
721 1
            && PhoneNumberUtil::normalizeDigitsOnly(
722 1
                \mb_substr($candidate, 0, $firstSlashInBodyIndex)
723 1
            ) == $number->getCountryCode()
724 1
        ) {
725
            // Any more slashes and this is illegal
726 1
            return (\mb_strpos(\mb_substr($candidate, $secondSlashInBodyIndex + 1), '/') !== false);
727
        }
728
729 1
        return true;
730
    }
731
732
    /**
733
     * @param PhoneNumber $number
734
     * @param string $candidate
735
     * @param PhoneNumberUtil $util
736
     * @return bool
737
     */
738 99
    public static function containsOnlyValidXChars(PhoneNumber $number, $candidate, PhoneNumberUtil $util)
739
    {
740
        // The characters 'x' and 'X' can be (1) a carrier code, in which case they always precede the
741
        // national significant number or (2) an extension sign, in which case they always precede the
742
        // extension number. We assume a carrier code is more than 1 digit, so the first case has to
743
        // have more than 1 consecutive 'x' or 'X', whereas the second case can only have exactly 1 'x'
744
        // or 'X'. We ignore the character if it appears as the last character of the string.
745 99
        $candidateLength = \mb_strlen($candidate);
746
747 99
        for ($index = 0; $index < $candidateLength - 1; $index++) {
748 99
            $charAtIndex = \mb_substr($candidate, $index, 1);
749 99
            if ($charAtIndex == 'x' || $charAtIndex == 'X') {
750 15
                $charAtNextIndex = \mb_substr($candidate, $index + 1, 1);
751 15
                if ($charAtNextIndex == 'x' || $charAtNextIndex == 'X') {
752
                    // This is the carrier code case, in which the 'X's always precede the national
753
                    // significant number.
754
                    $index++;
755
756
                    if ($util->isNumberMatch($number, \mb_substr($candidate, $index)) != MatchType::NSN_MATCH) {
757
                        return false;
758
                    }
759 15
                } elseif (!PhoneNumberUtil::normalizeDigitsOnly(\mb_substr($candidate,
760 15
                        $index)) == $number->getExtension()
761 15
                ) {
762
                    // This is the extension sign case, in which the 'x' or 'X' should always precede the
763
                    // extension number
764
                    return false;
765
                }
766 15
            }
767 99
        }
768 99
        return true;
769
    }
770
771
    /**
772
     * @param PhoneNumber $number
773
     * @param PhoneNumberUtil $util
774
     * @return bool
775
     */
776 99
    public static function isNationalPrefixPresentIfRequired(PhoneNumber $number, PhoneNumberUtil $util)
777
    {
778
        // First, check how we deduced the country code. If it was written in international format, then
779
        // the national prefix is not required.
780 99
        if ($number->getCountryCodeSource() !== CountryCodeSource::FROM_DEFAULT_COUNTRY) {
781 41
            return true;
782
        }
783
784 65
        $phoneNumberRegion = $util->getRegionCodeForCountryCode($number->getCountryCode());
785 65
        $metadata = $util->getMetadataForRegion($phoneNumberRegion);
786 65
        if ($metadata === null) {
787
            return true;
788
        }
789
790
        // Check if a national prefix should be present when formatting this number.
791 65
        $nationalNumber = $util->getNationalSignificantNumber($number);
792 65
        $formatRule = $util->chooseFormattingPatternForNumber($metadata->numberFormats(), $nationalNumber);
793
        // To do this, we check that a national prefix formatting rule was present and that it wasn't
794
        // just the first-group symbol ($1) with punctuation.
795 65
        if (($formatRule !== null) && \mb_strlen($formatRule->getNationalPrefixFormattingRule()) > 0) {
796 44
            if ($formatRule->getNationalPrefixOptionalWhenFormatting()) {
797
                // The national-prefix is optional in these cases, so we don't need to check if it was
798
                // present.
799 7
                return true;
800
            }
801
802 37
            if (PhoneNumberUtil::formattingRuleHasFirstGroupOnly($formatRule->getNationalPrefixFormattingRule())) {
803
                // National Prefix not needed for this number.
804 3
                return true;
805
            }
806
807
            // Normalize the remainder.
808 34
            $rawInputCopy = PhoneNumberUtil::normalizeDigitsOnly($number->getRawInput());
809 34
            $rawInput = $rawInputCopy;
810
            // Check if we found a national prefix and/or carrier code at the start of the raw input, and
811
            // return the result.
812 34
            $carrierCode = null;
813 34
            return $util->maybeStripNationalPrefixAndCarrierCode($rawInput, $metadata, $carrierCode);
814
        }
815 25
        return true;
816
    }
817
818
819
    /**
820
     * Storage for Alternate Formats
821
     * @var PhoneMetadata[]
822
     */
823
    protected static $callingCodeToAlternateFormatsMap = array();
824
825
    /**
826
     * @param $countryCallingCode
827
     * @return PhoneMetadata|null
828
     */
829 15
    protected static function getAlternateFormatsForCountry($countryCallingCode)
830
    {
831 15
        $countryCodeSet = AlternateFormatsCountryCodeSet::$alternateFormatsCountryCodeSet;
832
833 15
        if (!\in_array($countryCallingCode, $countryCodeSet)) {
834
            return null;
835
        }
836
837 15
        if (!isset(static::$callingCodeToAlternateFormatsMap[$countryCallingCode])) {
838 3
            static::loadAlternateFormatsMetadataFromFile($countryCallingCode);
839 3
        }
840
841 15
        return static::$callingCodeToAlternateFormatsMap[$countryCallingCode];
842
    }
843
844
    /**
845
     * @param string $countryCallingCode
846
     * @throws \Exception
847
     */
848 3
    protected static function loadAlternateFormatsMetadataFromFile($countryCallingCode)
849
    {
850 3
        $fileName = static::$alternateFormatsFilePrefix . '_' . $countryCallingCode . '.php';
851
852 3
        if (!\is_readable($fileName)) {
853
            throw new \Exception('missing metadata: ' . $fileName);
854
        }
855
856 3
        $metadataLoader = new DefaultMetadataLoader();
857 3
        $data = $metadataLoader->loadMetadata($fileName);
858 3
        $metadata = new PhoneMetadata();
859 3
        $metadata->fromArray($data);
860 3
        static::$callingCodeToAlternateFormatsMap[$countryCallingCode] = $metadata;
861 3
    }
862
863
864
    /**
865
     * Return the current element
866
     * @link http://php.net/manual/en/iterator.current.php
867
     * @return PhoneNumberMatch|null
868
     */
869 199
    public function current()
870
    {
871 199
        return $this->lastMatch;
872
    }
873
874
    /**
875
     * Move forward to next element
876
     * @link http://php.net/manual/en/iterator.next.php
877
     * @return void Any returned value is ignored.
878
     */
879 201
    public function next()
880
    {
881 201
        $this->lastMatch = $this->find($this->searchIndex);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $this->lastMatch is correct as $this->find($this->searchIndex) targeting libphonenumber\PhoneNumberMatcher::find() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
882
883 201
        if ($this->lastMatch === null) {
884 95
            $this->state = 'DONE';
885 95
        } else {
886 126
            $this->searchIndex = $this->lastMatch->end();
0 ignored issues
show
Bug introduced by
The method end() does not exist on null. ( Ignorable by Annotation )

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

886
            /** @scrutinizer ignore-call */ 
887
            $this->searchIndex = $this->lastMatch->end();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
887 126
            $this->state = 'READY';
888
        }
889
890 201
        $this->searchIndex++;
891 201
    }
892
893
    /**
894
     * Return the key of the current element
895
     * @link http://php.net/manual/en/iterator.key.php
896
     * @return mixed scalar on success, or null on failure.
897
     * @since 5.0.0
898
     */
899
    public function key()
900
    {
901
        return $this->searchIndex;
902
    }
903
904
    /**
905
     * Checks if current position is valid
906
     * @link http://php.net/manual/en/iterator.valid.php
907
     * @return boolean The return value will be casted to boolean and then evaluated.
908
     * Returns true on success or false on failure.
909
     * @since 5.0.0
910
     */
911 29
    public function valid()
912
    {
913 29
        return $this->state === 'READY';
914
    }
915
916
    /**
917
     * Rewind the Iterator to the first element
918
     * @link http://php.net/manual/en/iterator.rewind.php
919
     * @return void Any returned value is ignored.
920
     * @since 5.0.0
921
     */
922 18
    public function rewind()
923
    {
924 18
        $this->searchIndex = 0;
925 18
        $this->next();
926 18
    }
927
}
928