WildcardPattern::quote()   A
last analyzed

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 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
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 function implode;
8
use function preg_match;
9
use function preg_quote;
10
use function preg_replace;
11
use function strtr;
12
13
/**
14
 * A wildcard pattern to match strings against.
15
 *
16
 * - `\` escapes other special characters if usage of escape character is not turned off.
17
 * - `*` matches any string including the empty string except it has a delimiter (`/` and `\` by default).
18
 * - `**` matches any string including the empty string and delimiters.
19
 * - `?` matches any single character.
20
 * - `[seq]` matches any character in seq.
21
 * - `[a-z]` matches any character from a to z.
22
 * - `[!seq]` matches any character not in seq.
23
 * - `[[:alnum:]]` matches POSIX style character classes,
24
 *   see {@see https://www.php.net/manual/en/regexp.reference.character-classes.php}.
25
 */
26
final class WildcardPattern
27
{
28
    private bool $ignoreCase = false;
29
30
    /**
31
     * @psalm-var non-empty-string|null
32
     */
33
    private ?string $patternPrepared = null;
34
35
    /**
36
     * @param string $pattern The shell wildcard pattern to match against.
37
     * @param string[] $delimiters Delimiters to consider for "*" (`/` and `\` by default).
38
     */
39 59
    public function __construct(
40
        private string $pattern,
41
        private array $delimiters = ['\\\\', '/'],
42
    ) {
43 59
    }
44
45
    /**
46
     * Checks if the passed string would match the given shell wildcard pattern.
47
     *
48
     * @param string $string The tested string.
49
     *
50
     * @return bool Whether the string matches pattern or not.
51
     */
52 58
    public function match(string $string): bool
53
    {
54 58
        if ($this->pattern === '**') {
55 1
            return true;
56
        }
57
58 57
        return preg_match($this->getPatternPrepared(), $string) === 1;
59
    }
60
61
    /**
62
     * Make pattern case insensitive.
63
     */
64 3
    public function ignoreCase(bool $flag = true): self
65
    {
66 3
        $new = clone $this;
67 3
        $new->patternPrepared = null;
68 3
        $new->ignoreCase = $flag;
69
70 3
        return $new;
71
    }
72
73
    /**
74
     * Returns whether the pattern contains a dynamic part i.e.
75
     * has unescaped "*",  "{", "?", or "[" character.
76
     *
77
     * @param string $pattern The pattern to check.
78
     *
79
     * @return bool Whether the pattern contains a dynamic part.
80
     */
81 5
    public static function isDynamic(string $pattern): bool
82
    {
83 5
        $pattern = preg_replace('/\\\\./', '', $pattern);
84 5
        return preg_match('/[*{?\[]/', $pattern) === 1;
85
    }
86
87
    /**
88
     * Escapes pattern characters in a string.
89
     *
90
     * @param string $string Source string.
91
     *
92
     * @return string String with pattern characters escaped.
93
     */
94 2
    public static function quote(string $string): string
95
    {
96 2
        return preg_replace('#([\\\\?*\\[\\]])#', '\\\\$1', $string);
97
    }
98
99
    /**
100
     * @return non-empty-string
0 ignored issues
show
Documentation Bug introduced by
The doc comment non-empty-string at position 0 could not be parsed: Unknown type name 'non-empty-string' at position 0 in non-empty-string.
Loading history...
101
     */
102 57
    private function getPatternPrepared(): string
103
    {
104 57
        if ($this->patternPrepared !== null) {
105
            return $this->patternPrepared;
106
        }
107
108 57
        $replacements = [
109 57
            '\*\*' => '.*',
110 57
            '\\\\\\\\' => '\\\\',
111 57
            '\\\\\\*' => '[*]',
112 57
            '\\\\\\?' => '[?]',
113 57
            '\\\\\\[' => '[\[]',
114 57
            '\\\\\\]' => '[\]]',
115 57
        ];
116
117 57
        if ($this->delimiters === []) {
118 1
            $replacements += [
119 1
                '\*' => '.*',
120 1
                '\?' => '?',
121 1
            ];
122
        } else {
123 56
            $notDelimiters = '[^' . preg_quote(implode('', $this->delimiters), '#') . ']';
124 56
            $replacements += [
125 56
                '\*' => "$notDelimiters*",
126 56
                '\?' => $notDelimiters,
127 56
            ];
128
        }
129
130 57
        $replacements += [
131 57
            '\[\!' => '[^',
132 57
            '\[' => '[',
133 57
            '\]' => ']',
134 57
            '\-' => '-',
135 57
        ];
136
137 57
        $pattern = strtr(preg_quote($this->pattern, '#'), $replacements);
138 57
        $pattern = '#^' . $pattern . '$#us';
139
140 57
        if ($this->ignoreCase) {
141 1
            $pattern .= 'i';
142
        }
143
144 57
        $this->patternPrepared = $pattern;
145
146 57
        return $this->patternPrepared;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->patternPrepared returns the type null which is incompatible with the type-hinted return string.
Loading history...
147
    }
148
}
149