Passed
Push — master ( 272916...3e3561 )
by Alexander
01:44
created

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