Passed
Pull Request — master (#103)
by Dmitriy
11:15 queued 09:02
created

CombinedRegexp::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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