PathMatcher   A
last analyzed

Complexity

Total Complexity 30

Size/Duplication

Total Lines 189
Duplicated Lines 0 %

Test Coverage

Coverage 97.22%

Importance

Changes 4
Bugs 1 Features 1
Metric Value
eloc 70
c 4
b 1
f 1
dl 0
loc 189
ccs 70
cts 72
cp 0.9722
rs 10
wmc 30

9 Methods

Rating   Name   Duplication   Size   Complexity  
A except() 0 5 1
A match() 0 15 6
A matchExcept() 0 13 4
B prepareMatchers() 0 31 7
A doNotCheckFilesystem() 0 5 1
A caseSensitive() 0 5 1
A only() 0 5 1
A callback() 0 5 1
B matchOnly() 0 33 8
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Yiisoft\Files\PathMatcher;
6
7
use Yiisoft\Strings\StringHelper;
8
9
use function is_dir;
10
use function is_file;
11
use function str_ends_with;
12
use function strtr;
13
14
/**
15
 * Path matcher is based on {@see PathPattern} with the following logic:
16
 *
17
 *  1. Process `only()`. If there is at least one match, then continue, else return `false`;
18
 *  2. Process `except()`. If there is at least one match, return `false`, else continue;
19
 *  3. Process `callback()`. If there is at least one not match, return `false`, else return `true`.
20
 *
21
 * Either implementations of {@see PathMatcherInterface} or strings could be used in all above. They will be converted
22
 * {@see PathPattern} according to the options.
23
 *
24
 * If the string ends in `/`, then {@see PathPattern} will be created with {@see PathPattern::onlyDirectories()} option.
25
 * Else it will be create with {@see PathPattern::onlyFiles()} option. You can disable this behavior using
26
 * {@see PathMatcher::notCheckFilesystem()}.
27
 *
28
 * There are several other options available:
29
 *
30
 *  - {@see PathMatcher::caseSensitive()} makes string patterns case sensitive;
31
 *  - {@see PathMatcher::withFullPath()} string patterns will be matched as full path, not just as ending of the path;
32
 *  - {@see PathMatcher::withNotExactSlashes()} match `/` character with wildcards in string patterns.
33
 *
34
 * Usage example:
35
 *
36
 * ```php
37
 * $matcher = (new PathMatcher())
38
 *     ->notCheckFilesystem()
39
 *     ->only('*.css', '*.js')
40
 *     ->except('theme.css');
41
 *
42
 * $matcher->match('/var/www/example.com/assets/css/main.css'); // true
43
 * $matcher->match('/var/www/example.com/assets/css/main.css.map'); // false
44
 * $matcher->match('/var/www/example.com/assets/css/theme.css'); // false
45
 * ```
46
 */
47
final class PathMatcher implements PathMatcherInterface
48
{
49
    /**
50
     * @var PathMatcherInterface[]|null
51
     */
52
    private ?array $only = null;
53
54
    /**
55
     * @var PathMatcherInterface[]|null
56
     */
57
    private ?array $except = null;
58
59
    /**
60
     * @var callable[]|null
61
     */
62
    private ?array $callbacks = null;
63
64
    private bool $caseSensitive = false;
65
    private bool $checkFilesystem = true;
66
67
    /**
68
     * Make string patterns case sensitive.
69
     * Note: applies only to string patterns.
70
     */
71 2
    public function caseSensitive(): self
72
    {
73 2
        $new = clone $this;
74 2
        $new->caseSensitive = true;
75 2
        return $new;
76
    }
77
78
    /**
79
     * Match path only as string, do not check if file or directory exists.
80
     * Note: applies only to string patterns.
81
     */
82 12
    public function doNotCheckFilesystem(): self
83
    {
84 12
        $new = clone $this;
85 12
        $new->checkFilesystem = false;
86 12
        return $new;
87
    }
88
89
    /**
90
     * Set list of patterns that the files or directories should match.
91
     */
92 14
    public function only(PathMatcherInterface|string ...$patterns): self
93
    {
94 14
        $new = clone $this;
95 14
        $new->only = $this->prepareMatchers($patterns);
96 14
        return $new;
97
    }
98
99
    /**
100
     * Set list of patterns that the files or directories should not match.
101
     *
102
     * @see https://github.com/yiisoft/strings#wildcardpattern-usage
103
     *
104
     * @param PathMatcherInterface|string ...$patterns Simple POSIX-style string matching.
105
     */
106 7
    public function except(PathMatcherInterface|string ...$patterns): self
107
    {
108 7
        $new = clone $this;
109 7
        $new->except = $this->prepareMatchers($patterns);
110 7
        return $new;
111
    }
112
113
    /**
114
     * Set list of PHP callbacks that are called for each path.
115
     *
116
     * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
117
     * The callback should return `true` if there is a match and `false` otherwise.
118
     */
119 2
    public function callback(callable ...$callbacks): self
120
    {
121 2
        $new = clone $this;
122 2
        $new->callbacks = $callbacks;
123 2
        return $new;
124
    }
125
126
    /**
127
     * Checks if the passed path match specified conditions.
128
     *
129
     * @param string $path The tested path.
130
     *
131
     * @return bool Whether the path matches conditions or not.
132
     */
133 18
    public function match(string $path): bool
134
    {
135 18
        if (!$this->matchOnly($path) || $this->matchExcept($path)) {
136 15
            return false;
137
        }
138
139 18
        if ($this->callbacks !== null) {
140 1
            foreach ($this->callbacks as $callback) {
141 1
                if (!$callback($path)) {
142 1
                    return false;
143
                }
144
            }
145
        }
146
147 17
        return true;
148
    }
149
150 18
    private function matchOnly(string $path): bool
151
    {
152 18
        if ($this->only === null) {
153 5
            return true;
154
        }
155
156 13
        $hasFalse = false;
157 13
        $hasNull = false;
158
159 13
        foreach ($this->only as $pattern) {
160 13
            $match = $pattern->match($path);
161
162 13
            if ($match === true) {
163 12
                return true;
164
            }
165
166 12
            if ($match === false) {
167 12
                $hasFalse = true;
168
            } else {
169 5
                $hasNull = true;
170
            }
171
        }
172
173 12
        if ($this->checkFilesystem) {
174 6
            if (is_file($path)) {
175 5
                return !$hasFalse;
176
            }
177 6
            if (is_dir($path)) {
178 6
                return $hasNull;
179
            }
180
        }
181
182 7
        return false;
183
    }
184
185 18
    private function matchExcept(string $path): bool
186
    {
187 18
        if ($this->except === null) {
188 12
            return false;
189
        }
190
191 6
        foreach ($this->except as $pattern) {
192 6
            if ($pattern->match($path) === true) {
193 6
                return true;
194
            }
195
        }
196
197 6
        return false;
198
    }
199
200
    /**
201
     * @param PathMatcherInterface[]|string[] $patterns
202
     *
203
     * @return PathMatcherInterface[]
204
     */
205 17
    private function prepareMatchers(array $patterns): array
206
    {
207 17
        $pathPatterns = [];
208 17
        foreach ($patterns as $pattern) {
209 17
            if ($pattern instanceof PathMatcherInterface) {
210
                $pathPatterns[] = $pattern;
211
                continue;
212
            }
213
214 17
            $pattern = strtr($pattern, '\\', '/');
215 17
            $isDirectoryPattern = str_ends_with($pattern, '/');
216
217 17
            if ($isDirectoryPattern) {
218 8
                $pattern = StringHelper::substring($pattern, 0, -1);
219
            }
220
221 17
            $pathPattern = new PathPattern($pattern);
222
223 17
            if ($this->caseSensitive) {
224 1
                $pathPattern = $pathPattern->caseSensitive();
225
            }
226
227 17
            if ($this->checkFilesystem) {
228 9
                $pathPattern = $isDirectoryPattern
229 7
                    ? $pathPattern->onlyDirectories()
230 6
                    : $pathPattern->onlyFiles();
231
            }
232
233 17
            $pathPatterns[] = $pathPattern;
234
        }
235 17
        return $pathPatterns;
236
    }
237
}
238