Passed
Pull Request — master (#22)
by Sergei
03:02
created

PathMatcher::notCheckFilesystem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

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