Passed
Pull Request — master (#67)
by Alexander
02:03
created

WildcardPattern::isDynamic()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 5
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 7
ccs 5
cts 5
cp 1
crap 4
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Strings;
6
7
/**
8
 * A wildcard pattern to match strings against.
9
 *
10
 * - `\` escapes other special characters if usage of escape character is not turned off.
11
 * - `*` matches any string including the empty string except it has a delimiter (`/` and `\` by default).
12
  * - `**` matches any string including the empty string and delimiters.
13
 * - `?` matches any single character.
14
 * - `[seq]` matches any character in seq.
15
 * - `[a-z]` matches any character from a to z.
16
 * - `[!seq]` matches any character not in seq.
17
 * - `[[:alnum:]]` matches POSIX style character classes,
18
 *   see {@see https://www.php.net/manual/en/regexp.reference.character-classes.php}.
19
 */
20
final class WildcardPattern
21
{
22
    private bool $withoutEscape = false;
23
    private bool $matchLeadingPeriodExactly = false;
24
    private bool $ignoreCase = false;
25
    private bool $matchEnding = false;
26
    private string $pattern;
27
    private array $delimiters;
28
29
    /**
30
     * @param string $pattern The shell wildcard pattern to match against.
31
     * @param array $delimiters Delimiters to consider for "*" (`/` and `\` by default).
32
     */
33 68
    public function __construct(string $pattern, array $delimiters = ['\\\\', '/'])
34
    {
35 68
        $this->pattern = $pattern;
36 68
        $this->delimiters = $delimiters;
37 68
    }
38
39
    /**
40
     * Checks if the passed string would match the given shell wildcard pattern.
41
     *
42
     * @param string $string The tested string.
43
     *
44
     * @return bool Whether the string matches pattern or not.
45
     */
46 67
    public function match(string $string): bool
47
    {
48 67
        if ($this->pattern === '**' && !$this->matchLeadingPeriodExactly) {
49
            return true;
50
        }
51
52 67
        $pattern = $this->pattern;
53
54 67
        if ($this->matchLeadingPeriodExactly) {
55 3
            $pattern = preg_replace('/^[*?]/', '[!.]', $pattern);
56
        }
57
58 67
        $notDelimiters = '[^' . preg_quote(implode('', $this->delimiters), '#') . ']';
59
60
        $replacements = [
61 67
            '\*\*' => '.*',
62 67
            '\\\\\\\\' => '\\\\',
63 67
            '\\\\\\*' => '[*]',
64 67
            '\\\\\\?' => '[?]',
65 67
            '\*' => "$notDelimiters*",
66 67
            '\?' => $notDelimiters,
67 67
            '\[\!' => '[^',
68 67
            '\[' => '[',
69 67
            '\]' => ']',
70 67
            '\-' => '-',
71
        ];
72
73 67
        if ($this->withoutEscape) {
74 5
            unset($replacements['\\\\\\\\'], $replacements['\\\\\\*'], $replacements['\\\\\\?']);
75
        }
76
77 67
        $pattern = strtr(preg_quote($pattern, '#'), $replacements);
78 67
        $pattern = '#' . ($this->matchEnding ? '' : '^') . $pattern . '$#us';
79
80 67
        if ($this->ignoreCase) {
81 1
            $pattern .= 'i';
82
        }
83
84 67
        return preg_match($pattern, $string) === 1;
85
    }
86
87
    /**
88
     * Disables using `\` to escape following special character. `\` becomes regular character.
89
     *
90
     * @param bool $flag
91
     *
92
     * @return self
93
     */
94 7
    public function withoutEscape(bool $flag = true): self
95
    {
96 7
        $new = clone $this;
97 7
        $new->withoutEscape = $flag;
98 7
        return $new;
99
    }
100
101
    /**
102
     * Make pattern case insensitive.
103
     *
104
     * @param bool $flag
105
     *
106
     * @return self
107
     */
108 3
    public function ignoreCase(bool $flag = true): self
109
    {
110 3
        $new = clone $this;
111 3
        $new->ignoreCase = $flag;
112 3
        return $new;
113
    }
114
115
    /**
116
     * Do not match `.` character at the beginning of string with wildcards.
117
     * Useful for matching file paths.
118
     *
119
     * @param bool $flag
120
     *
121
     * @return self
122
     */
123 5
    public function exactLeadingPeriod(bool $flag = true): self
124
    {
125 5
        $new = clone $this;
126 5
        $new->matchLeadingPeriodExactly = $flag;
127 5
        return $new;
128
    }
129
130
    /**
131
     * Match ending only.
132
     * By default wildcard pattern matches string exactly. By using this mode, beginning of the string could be anything.
133
     *
134
     * @param bool $flag
135
     *
136
     * @return self
137
     */
138 8
    public function matchEnding(bool $flag = true): self
139
    {
140 8
        $new = clone $this;
141 8
        $new->matchEnding = $flag;
142 8
        return $new;
143
    }
144
145
    /**
146
     * Returns whether the pattern contains a dynamic part i.e.
147
     * has unescaped "*",  "{", "?", or "[" character.
148
     *
149
     * @param string $pattern The pattern to check.
150
     *
151
     * @return bool Whether the pattern contains a dynamic part.
152
     */
153 5
    public static function isDynamic(string $pattern): bool
154
    {
155
        return
156 5
            self::hasDynamicCharacter($pattern, '*')
157 5
            || self::hasDynamicCharacter($pattern, '{')
158 5
            || self::hasDynamicCharacter($pattern, '?')
159 5
            || self::hasDynamicCharacter($pattern, '[');
160
    }
161
162 5
    private static function hasDynamicCharacter(string $pattern, string $character): bool
163
    {
164 5
        $position = strpos($pattern, $character);
165 5
        if ($position !== false) {
166 4
            if ($position === 0) {
167
                return true;
168
            }
169
170 4
            $slashesCount = 0;
171 4
            $checkPosition = $position - 1;
172 4
            while ($checkPosition > 0) {
173 4
                if ($pattern[$checkPosition] === '\\') {
174 2
                    $slashesCount++;
175
                }
176 4
                $checkPosition--;
177
            }
178
179 4
            if ($slashesCount % 2 === 0) {
180 3
                return true;
181
            }
182
        }
183
184 5
        return false;
185
    }
186
}
187