Passed
Pull Request — master (#22)
by Sergei
25:20 queued 10:23
created

PathMatcher::matchExcept()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

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