Passed
Push — master ( 55204c...64475d )
by Alexander
23:33 queued 21:19
created

CombinedRegexp::matches()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Strings;
6
7
use Exception;
8
use InvalidArgumentException;
9
10
use function count;
11
12
/**
13
 * `CombinedRegexp` optimizes matching of multiple regular expressions.
14
 * Read more about the concept in
15
 * {@see https://nikic.github.io/2014/02/18/Fast-request-routing-using-regular-expressions.html}.
16
 */
17
final class CombinedRegexp extends AbstractCombinedRegexp
18
{
19
    /**
20
     * @var string[]
21
     */
22
    private array $patterns;
23
    /**
24
     * @psalm-var non-empty-string
25
     */
26
    private string $compiledPattern;
27
28
    /**
29
     * @param string[] $patterns Regular expressions to combine.
30
     * @param string $flags Flags to apply to all regular expressions.
31
     */
32 38
    public function __construct(
33
        array $patterns,
34
        private string $flags = ''
35
    ) {
36 38
        if (empty($patterns)) {
37 2
            throw new InvalidArgumentException('At least one pattern should be specified.');
38
        }
39 36
        $this->patterns = array_values($patterns);
40 36
        $this->compiledPattern = $this->compilePatterns($this->patterns) . $this->flags;
41
    }
42
43
    /**
44
     * @return string The compiled pattern.
45
     */
46 36
    public function getCompiledPattern(): string
47
    {
48 36
        return $this->compiledPattern;
49
    }
50
51
    /**
52
     * Returns `true` whether the given string matches any of the patterns, `false` - otherwise.
53
     */
54 12
    public function matches(string $string): bool
55
    {
56 12
        return preg_match($this->compiledPattern, $string) === 1;
57
    }
58
59
    /**
60
     * Returns pattern that matches the given string.
61
     * @throws Exception if the string does not match any of the patterns.
62
     */
63 5
    public function getMatchingPattern(string $string): string
64
    {
65 5
        return $this->patterns[$this->getMatchingPatternPosition($string)];
66
    }
67
68
    /**
69
     * Returns position of the pattern that matches the given string.
70
     * @throws Exception if the string does not match any of the patterns.
71
     */
72 27
    public function getMatchingPatternPosition(string $string): int
73
    {
74 27
        $match = preg_match($this->compiledPattern, $string, $matches);
75 27
        if ($match !== 1) {
76 3
            $this->throwFailedMatchException($string);
77
        }
78
79 24
        return count($matches) - 1;
80
    }
81
82
    /**
83
     * @param string[] $patterns
84
     *
85
     * @psalm-param list<string> $patterns
86
     * @psalm-return non-empty-string
87
     */
88 36
    private function compilePatterns(array $patterns): string
89
    {
90 36
        $quotedPatterns = [];
91
92
        /**
93
         * Possible mutant escaping, but it's ok for our case.
94
         * It doesn't matter where to place `()` in the pattern:
95
         * https://regex101.com/r/lE1Q1S/1, https://regex101.com/r/rWg7Fj/1
96
         */
97 36
        foreach ($patterns as $i => $pattern) {
98 36
            $quotedPatterns[] = $pattern . str_repeat('()', $i);
99
        }
100 36
        $combinedRegexps = '(?|' . strtr(
101 36
            implode('|', $quotedPatterns),
102 36
            [self::REGEXP_DELIMITER => self::QUOTE_REPLACER]
103 36
        ) . ')';
104
105 36
        return self::REGEXP_DELIMITER . $combinedRegexps . self::REGEXP_DELIMITER;
106
    }
107
108 5
    public function getPatterns(): array
109
    {
110 5
        return $this->patterns;
111
    }
112
113
    public function getFlags(): string
114
    {
115
        return $this->flags;
116
    }
117
}
118