Passed
Pull Request — master (#22)
by Sergei
02:23 queued 29s
created

PathMatcher   A

Complexity

Total Complexity 37

Size/Duplication

Total Lines 240
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 82
dl 0
loc 240
ccs 85
cts 85
cp 1
rs 9.44
c 2
b 0
f 0
wmc 37

11 Methods

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