Passed
Pull Request — master (#55)
by Alexander
02:04
created

PathMatcher   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 205
Duplicated Lines 0 %

Test Coverage

Coverage 97.3%

Importance

Changes 4
Bugs 0 Features 1
Metric Value
eloc 72
c 4
b 0
f 1
dl 0
loc 205
ccs 72
cts 74
cp 0.973
rs 9.92
wmc 31

9 Methods

Rating   Name   Duplication   Size   Complexity  
A doNotCheckFilesystem() 0 5 1
A caseSensitive() 0 5 1
A only() 0 5 1
A except() 0 5 1
A match() 0 19 6
A matchExcept() 0 13 4
A callback() 0 5 1
B matchOnly() 0 31 9
B prepareMatchers() 0 31 7
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 $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 path only as string, do not check if file or directory exists.
77
     * Note: applies only to string patterns.
78
     *
79
     * @return self
80
     */
81 12
    public function doNotCheckFilesystem(): self
82
    {
83 12
        $new = clone $this;
84 12
        $new->checkFilesystem = false;
85 12
        return $new;
86
    }
87
88
    /**
89
     * Set list of patterns that the files or directories should match.
90
     *
91
     * @param PathMatcherInterface|string ...$patterns
92
     *
93
     * @return self
94
     */
95 13
    public function only(...$patterns): self
96
    {
97 13
        $new = clone $this;
98 13
        $new->only = $this->prepareMatchers($patterns);
99 13
        return $new;
100
    }
101
102
    /**
103
     * Set list of patterns that the files or directories should not match.
104
     *
105
     * @see https://github.com/yiisoft/strings#wildcardpattern-usage
106
     *
107
     * @param PathMatcherInterface|string ...$patterns Simple POSIX-style string matching.
108
     *
109
     * @return self
110
     */
111 6
    public function except(...$patterns): self
112
    {
113 6
        $new = clone $this;
114 6
        $new->except = $this->prepareMatchers($patterns);
115 6
        return $new;
116
    }
117
118
    /**
119
     * Set list of PHP callbacks that are called for each path.
120
     *
121
     * The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
122
     * The callback should return `true` if there is a match and `false` otherwise.
123
     *
124
     * @param callable ...$callbacks
125
     *
126
     * @return self
127
     */
128 2
    public function callback(callable ...$callbacks): self
129
    {
130 2
        $new = clone $this;
131 2
        $new->callbacks = $callbacks;
132 2
        return $new;
133
    }
134
135
    /**
136
     * Checks if the passed path match specified conditions.
137
     *
138
     * @param string $path The tested path.
139
     *
140
     * @return bool Whether the path matches conditions or not.
141
     */
142 17
    public function match(string $path): bool
143
    {
144 17
        if (!$this->matchOnly($path)) {
145 11
            return false;
146
        }
147
148 17
        if ($this->matchExcept($path)) {
149 5
            return false;
150
        }
151
152 17
        if ($this->callbacks !== null) {
153 1
            foreach ($this->callbacks as $callback) {
154 1
                if (!$callback($path)) {
155 1
                    return false;
156
                }
157
            }
158
        }
159
160 16
        return true;
161
    }
162
163 17
    private function matchOnly(string $path): bool
164
    {
165 17
        if ($this->only === null) {
166 5
            return true;
167
        }
168
169 12
        $hasFalse = false;
170 12
        $hasNull = false;
171
172 12
        foreach ($this->only as $pattern) {
173 12
            if ($pattern->match($path) === true) {
174 11
                return true;
175
            }
176 11
            if ($pattern->match($path) === false) {
177 11
                $hasFalse = true;
178
            }
179 11
            if ($pattern->match($path) === null) {
180 4
                $hasNull = true;
181
            }
182
        }
183
184 11
        if ($this->checkFilesystem) {
185 5
            if (is_file($path)) {
186 4
                return !$hasFalse;
187
            }
188 5
            if (is_dir($path)) {
189 5
                return $hasNull;
190
            }
191
        }
192
193 7
        return false;
194
    }
195
196 17
    private function matchExcept(string $path): bool
197
    {
198 17
        if ($this->except === null) {
199 12
            return false;
200
        }
201
202 5
        foreach ($this->except as $pattern) {
203 5
            if ($pattern->match($path) === true) {
204 5
                return true;
205
            }
206
        }
207
208 5
        return false;
209
    }
210
211
    /**
212
     * @param PathMatcherInterface[]|string[] $patterns
213
     *
214
     * @return PathMatcherInterface[]
215
     */
216 16
    private function prepareMatchers(array $patterns): array
217
    {
218 16
        $pathPatterns = [];
219 16
        foreach ($patterns as $pattern) {
220 16
            if ($pattern instanceof PathMatcherInterface) {
221
                $pathPatterns[] = $pattern;
222
                continue;
223
            }
224
225 16
            $pattern = strtr($pattern, '/\\', '//');
226
227 16
            $isDirectoryPattern = StringHelper::endsWith($pattern, '/');
228 16
            if ($isDirectoryPattern) {
229 7
                $pattern = StringHelper::substring($pattern, 0, -1);
230
            }
231
232 16
            $pathPattern = new PathPattern($pattern);
233
234 16
            if ($this->caseSensitive) {
235 1
                $pathPattern = $pathPattern->caseSensitive();
236
            }
237
238 16
            if ($this->checkFilesystem) {
239 8
                $pathPattern = $isDirectoryPattern
240 6
                    ? $pathPattern->onlyDirectories()
241 8
                    : $pathPattern->onlyFiles();
242
            }
243
244 16
            $pathPatterns[] = $pathPattern;
245
        }
246 16
        return $pathPatterns;
247
    }
248
}
249