Failed Conditions
Push — master ( 46067f...a89cea )
by Bernhard
02:38
created

Glob::match()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0313

Importance

Changes 3
Bugs 1 Features 0
Metric Value
c 3
b 1
f 0
dl 0
loc 16
ccs 7
cts 8
cp 0.875
rs 9.2
cc 4
eloc 8
nc 4
nop 3
crap 4.0313
1
<?php
2
3
/*
4
 * This file is part of the webmozart/glob package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Webmozart\Glob;
13
14
use InvalidArgumentException;
15
use Webmozart\Glob\Iterator\GlobIterator;
16
use Webmozart\PathUtil\Path;
17
18
/**
19
 * Searches and matches file paths using Ant-like globs.
20
 *
21
 * This class implements an Ant-like version of PHP's `glob()` function. The
22
 * wildcard "*" matches any number of characters except directory separators.
23
 * The double wildcard "**" matches any number of characters, including
24
 * directory separators.
25
 *
26
 * Use {@link glob()} to glob the filesystem for paths:
27
 *
28
 * ```php
29
 * foreach (Glob::glob('/project/**.twig') as $path) {
30
 *     // do something...
31
 * }
32
 * ```
33
 *
34
 * Use {@link match()} to match a file path against a glob:
35
 *
36
 * ```php
37
 * if (Glob::match('/project/views/index.html.twig', '/project/**.twig')) {
38
 *     // path matches
39
 * }
40
 * ```
41
 *
42
 * You can also filter an array of paths for all paths that match your glob with
43
 * {@link filter()}:
44
 *
45
 * ```php
46
 * $filteredPaths = Glob::filter($paths, '/project/**.twig');
47
 * ```
48
 *
49
 * Internally, the methods described above convert the glob into a regular
50
 * expression that is then matched against the matched paths. If you need to
51
 * match many paths against the same glob, you should convert the glob manually
52
 * and use {@link preg_match()} to test the paths:
53
 *
54
 * ```php
55
 * $staticPrefix = Glob::getStaticPrefix('/project/**.twig');
56
 * $regEx = Glob::toRegEx('/project/**.twig');
57
 *
58
 * if (0 !== strpos($path, $staticPrefix)) {
59
 *     // no match
60
 * }
61
 *
62
 * if (!preg_match($regEx, $path)) {
63
 *     // no match
64
 * }
65
 * ```
66
 *
67
 * The method {@link getStaticPrefix()} returns the part of the glob up to the
68
 * first wildcard "*". You should always test whether a path has this prefix
69
 * before calling the much more expensive {@link preg_match()}.
70
 *
71
 * @since  1.0
72
 *
73
 * @author Bernhard Schussek <[email protected]>
74
 */
75
final class Glob
76
{
77
    /**
78
     * Flag: Match the keys instead of the values in {@link Glob::filter()}
79
     */
80
    const MATCH_KEYS = 1;
81
82
    /**
83
     * Globs the file system paths matching the glob.
84
     *
85
     * The glob may contain the wildcard "*". This wildcard matches any number
86
     * of characters, *including* directory separators.
87
     *
88
     * ```php
89
     * foreach (Glob::glob('/project/**.twig') as $path) {
90
     *     // do something...
91
     * }
92
     * ```
93
     *
94
     * @param string $glob  The canonical glob. The glob should contain forward
95
     *                      slashes as directory separators only. It must not
96
     *                      contain any "." or ".." segments. Use the
97
     *                      "webmozart/path-util" utility to canonicalize globs
98
     *                      prior to calling this method.
99
     * @param int    $flags A bitwise combination of the flag constants in this
100
     *                      class.
101
     *
102
     * @return string[] The matching paths. The keys of the array are
103
     *                  incrementing integers.
104
     */
105 8
    public static function glob($glob, $flags = 0)
106
    {
107 8
        $results = iterator_to_array(new GlobIterator($glob, $flags));
108
109 3
        sort($results);
110
111 3
        return $results;
112
    }
113
114
    /**
115
     * Matches a path against a glob.
116
     *
117
     * ```php
118
     * if (Glob::match('/project/views/index.html.twig', '/project/**.twig')) {
119
     *     // path matches
120
     * }
121
     * ```
122
     *
123
     * @param string $path  The path to match.
124
     * @param string $glob  The canonical glob. The glob should contain forward
125
     *                      slashes as directory separators only. It must not
126
     *                      contain any "." or ".." segments. Use the
127
     *                      "webmozart/path-util" utility to canonicalize globs
128
     *                      prior to calling this method.
129
     * @param int    $flags A bitwise combination of the flag constants in
130
     *                      this class.
131
     *
132
     * @return bool Returns `true` if the path is matched by the glob.
133
     */
134 12
    public static function match($path, $glob, $flags = 0)
135
    {
136 12
        if (!self::isDynamic($glob)) {
137 1
            return $glob === $path;
138
        }
139
140 11
        if (0 !== strpos($path, self::getStaticPrefix($glob, $flags))) {
141 6
            return false;
142
        }
143
144 5
        if (!preg_match(self::toRegEx($glob, $flags), $path)) {
0 ignored issues
show
Unused Code introduced by
This if statement, and the following return statement can be replaced with return (bool) preg_match...$glob, $flags), $path);.
Loading history...
145
            return false;
146
        }
147
148 5
        return true;
149
    }
150
151
    /**
152
     * Filters an array for paths matching a glob.
153
     *
154
     * The filtered array is returned. This array preserves the keys of the
155
     * passed array.
156
     *
157
     * ```php
158
     * $filteredPaths = Glob::filter($paths, '/project/**.twig');
159
     * ```
160
     *
161
     * @param string[] $paths A list of paths.
162
     * @param string   $glob  The canonical glob. The glob should contain
163
     *                        forward slashes as directory separators only. It
164
     *                        must not contain any "." or ".." segments. Use the
165
     *                        "webmozart/path-util" utility to canonicalize
166
     *                        globs prior to calling this method.
167
     * @param int      $flags A bitwise combination of the flag constants in
168
     *                        this class.
169
     *
170
     * @return string[] The paths matching the glob indexed by their original
171
     *                  keys.
172
     */
173 8
    public static function filter(array $paths, $glob, $flags = 0)
174
    {
175 8
        if (!self::isDynamic($glob)) {
176 2
            if ($flags & self::MATCH_KEYS) {
177 1
                return isset($paths[$glob]) ? array($glob => $paths[$glob]) : array();
178
            }
179
180 1
            $key = array_search($glob, $paths);
181
182 1
            return false !== $key ? array($key => $glob) : array();
183
        }
184
185 6
        $staticPrefix = self::getStaticPrefix($glob, $flags);
186 4
        $regExp = self::toRegEx($glob, $flags);
187 4
        $filter = function ($path) use ($staticPrefix, $regExp) {
188 4
            return 0 === strpos($path, $staticPrefix) && preg_match($regExp, $path);
189 4
        };
190
191 4
        if (PHP_VERSION_ID >= 50600) {
192
            $filterFlags = ($flags & self::MATCH_KEYS) ? ARRAY_FILTER_USE_KEY : 0;
193
194
            return array_filter($paths, $filter, $filterFlags);
195
        }
196
197
        // No support yet for the third argument of array_filter()
198 4
        if ($flags & self::MATCH_KEYS) {
199 2
            $result = array();
200
201 2
            foreach ($paths as $path => $value) {
202 2
                if ($filter($path)) {
203 2
                    $result[$path] = $value;
204 2
                }
205 2
            }
206
207 2
            return $result;
208
        }
209
210 2
        return array_filter($paths, $filter);
211
    }
212
213
    /**
214
     * Returns the base path of a glob.
215
     *
216
     * This method returns the most specific directory that contains all files
217
     * matched by the glob. If this directory does not exist on the file system,
218
     * it's not necessary to execute the glob algorithm.
219
     *
220
     * More specifically, the "base path" is the longest path trailed by a "/"
221
     * on the left of the first wildcard "*". If the glob does not contain
222
     * wildcards, the directory name of the glob is returned.
223
     *
224
     * ```php
225
     * Glob::getBasePath('/css/*.css');
226
     * // => /css
227
     *
228
     * Glob::getBasePath('/css/style.css');
229
     * // => /css
230
     *
231
     * Glob::getBasePath('/css/st*.css');
232
     * // => /css
233
     *
234
     * Glob::getBasePath('/*.css');
235
     * // => /
236
     * ```
237
     *
238
     * @param string $glob  The canonical glob. The glob should contain forward
239
     *                      slashes as directory separators only. It must not
240
     *                      contain any "." or ".." segments. Use the
241
     *                      "webmozart/path-util" utility to canonicalize globs
242
     *                      prior to calling this method.
243
     * @param int    $flags A bitwise combination of the flag constants in this
244
     *                      class.
245
     *
246
     * @return string The base path of the glob.
247
     */
248 42
    public static function getBasePath($glob, $flags = 0)
249
    {
250
        // Search the static prefix for the last "/"
251 42
        $staticPrefix = self::getStaticPrefix($glob, $flags);
252
253 40
        if (false !== ($pos = strrpos($staticPrefix, '/'))) {
254
            // Special case: Return "/" if the only slash is at the beginning
255
            // of the glob
256 40
            if (0 === $pos) {
257 2
                return '/';
258
            }
259
260
            // Special case: Include trailing slash of "scheme:///foo"
261 38
            if ($pos - 3 === strpos($glob, '://')) {
262 3
                return substr($staticPrefix, 0, $pos + 1);
263
            }
264
265 35
            return substr($staticPrefix, 0, $pos);
266
        }
267
268
        // Glob contains no slashes on the left of the wildcard
269
        // Return an empty string
270
        return '';
271
    }
272
273
    /**
274
     * Converts a glob to a regular expression.
275
     *
276
     * Use this method if you need to match many paths against a glob:
277
     *
278
     * ```php
279
     * $staticPrefix = Glob::getStaticPrefix('/project/**.twig');
280
     * $regEx = Glob::toRegEx('/project/**.twig');
281
     *
282
     * if (0 !== strpos($path, $staticPrefix)) {
283
     *     // no match
284
     * }
285
     *
286
     * if (!preg_match($regEx, $path)) {
287
     *     // no match
288
     * }
289
     * ```
290
     *
291
     * You should always test whether a path contains the static prefix of the
292
     * glob returned by {@link getStaticPrefix()} to reduce the number of calls
293
     * to the expensive {@link preg_match()}.
294
     *
295
     * @param string $glob  The canonical glob. The glob should contain forward
296
     *                      slashes as directory separators only. It must not
297
     *                      contain any "." or ".." segments. Use the
298
     *                      "webmozart/path-util" utility to canonicalize globs
299
     *                      prior to calling this method.
300
     * @param int    $flags A bitwise combination of the flag constants in this
301
     *                      class.
302
     *
303
     * @return string The regular expression for matching the glob.
304
     */
305 75
    public static function toRegEx($glob, $flags = 0, $delimiter = '~')
0 ignored issues
show
Unused Code introduced by
The parameter $flags is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
306
    {
307 75 View Code Duplication
        if (!Path::isAbsolute($glob) && false === strpos($glob, '://')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
308 1
            throw new InvalidArgumentException(sprintf(
309 1
                'The glob "%s" is not absolute and not a URI.',
310
                $glob
311 1
            ));
312
        }
313
314 74
        $inSquare = false;
315 74
        $curlyLevels = 0;
316 74
        $regex = '';
317 74
        $length = strlen($glob);
318
319 74
        for ($i = 0; $i < $length; ++$i) {
320 74
            $c = $glob[$i];
321
322
            switch ($c) {
323 74
                case '.':
324 74
                case '(':
325 74
                case ')':
326 74
                case '|':
327 74
                case '+':
328 74
                case '^':
329 74
                case '$':
330 74
                case $delimiter:
331 71
                    $regex .= "\\$c";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $c instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
332 71
                    break;
333
334 74 View Code Duplication
                case '/':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
335 74
                    if (isset($glob[$i + 3]) && '**/' === $glob[$i + 1].$glob[$i + 2].$glob[$i + 3]) {
336 24
                        $regex .= '/([^/]+/)*';
337 24
                        $i += 3;
338 24
                    } else {
339 72
                        $regex .= '/';
340
                    }
341 74
                    break;
342
343 74
                case '*':
344 40
                    $regex .= '[^/]*';
345 40
                    break;
346
347 74
                case '?':
348 3
                    $regex .= '.';
349 3
                    break;
350
351 74
                case '{':
352 8
                    $regex .= '(';
353 8
                    ++$curlyLevels;
354 8
                    break;
355
356 74
                case '}':
357 8
                    if ($curlyLevels > 0) {
358 6
                        $regex .= ')';
359 6
                        --$curlyLevels;
360 6
                    } else {
361 3
                        $regex .= '}';
362
                    }
363 8
                    break;
364
365 74
                case ',':
366 9
                    $regex .= $curlyLevels > 0 ? '|' : ',';
367 9
                    break;
368
369 74
                case '[':
370 12
                    $regex .= '[';
371 12
                    $inSquare = true;
372 12
                    if (isset($glob[$i + 1]) && '^' === $glob[$i + 1]) {
373 1
                        $regex .= '^';
374 1
                        ++$i;
375 1
                    }
376 12
                    break;
377
378 74
                case ']':
379 12
                    $regex .= $inSquare ? ']' : '\\]';
380 12
                    $inSquare = false;
381 12
                    break;
382
383 74
                case '-':
384 13
                    $regex .= $inSquare ? '-' : '\\-';
385 13
                    break;
386
387 74
                case '\\':
388 33
                    if (isset($glob[$i + 1])) {
389 33
                        switch ($glob[$i + 1]) {
390 33
                            case '*':
391 33
                            case '?':
392 33
                            case '{':
393 33
                            case '}':
394 33
                            case '[':
395 33
                            case ']':
396 33
                            case '-':
397 33
                            case '^':
398 33
                            case '\\':
399 33
                                $regex .= '\\'.$glob[$i + 1];
400 33
                                ++$i;
401 33
                                break;
402
403
                            default:
404
                                $regex .= '\\\\';
405 33
                        }
406 33
                    } else {
407
                        $regex .= '\\\\';
408
                    }
409 33
                    break;
410
411 74
                default:
412 74
                    $regex .= $c;
413 74
                    break;
414 74
            }
415 74
        }
416
417 74
        if ($inSquare) {
418 2
            throw new InvalidArgumentException(sprintf(
419 2
                'Invalid glob: missing ] in %s',
420
                $glob
421 2
            ));
422
        }
423
424 72
        if ($curlyLevels > 0) {
425 2
            throw new InvalidArgumentException(sprintf(
426 2
                'Invalid glob: missing } in %s',
427
                $glob
428 2
            ));
429
        }
430
431 70
        return $delimiter.'^'.$regex.'$'.$delimiter;
432
    }
433
434
    /**
435
     * Returns the static prefix of a glob.
436
     *
437
     * The "static prefix" is the part of the glob up to the first wildcard "*".
438
     * If the glob does not contain wildcards, the full glob is returned.
439
     *
440
     * @param string $glob  The canonical glob. The glob should contain forward
441
     *                      slashes as directory separators only. It must not
442
     *                      contain any "." or ".." segments. Use the
443
     *                      "webmozart/path-util" utility to canonicalize globs
444
     *                      prior to calling this method.
445
     * @param int    $flags A bitwise combination of the flag constants in this
446
     *                      class.
447
     *
448
     * @return string The static prefix of the glob.
449
     */
450 81
    public static function getStaticPrefix($glob, $flags = 0)
0 ignored issues
show
Unused Code introduced by
The parameter $flags is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
451
    {
452 81 View Code Duplication
        if (!Path::isAbsolute($glob) && false === strpos($glob, '://')) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
453 6
            throw new InvalidArgumentException(sprintf(
454 6
                'The glob "%s" is not absolute and not a URI.',
455
                $glob
456 6
            ));
457
        }
458
459 75
        $prefix = '';
460 75
        $length = strlen($glob);
461
462 75
        for ($i = 0; $i < $length; ++$i) {
463 75
            $c = $glob[$i];
464
465
            switch ($c) {
466 75 View Code Duplication
                case '/':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
467 75
                    $prefix .= '/';
468 75
                    if (isset($glob[$i + 3]) && '**/' === $glob[$i + 1].$glob[$i + 2].$glob[$i + 3]) {
469 20
                        break 2;
470
                    }
471 73
                    break;
472
473 73
                case '*':
474 73
                case '?':
475 73
                case '{':
476 73
                case '[':
477 35
                    break 2;
478
479 72
                case '\\':
480 29
                    if (isset($glob[$i + 1])) {
481 29
                        switch ($glob[$i + 1]) {
482 29
                            case '*':
483 29
                            case '?':
484 29
                            case '{':
485 29
                            case '[':
486 29
                            case '\\':
487 29
                                $prefix .= $glob[$i + 1];
488 29
                                ++$i;
489 29
                                break;
490
491 1
                            default:
492 1
                                $prefix .= '\\';
493 29
                        }
494 29
                    } else {
495
                        $prefix .= '\\';
496
                    }
497 29
                    break;
498
499 72
                default:
500 72
                    $prefix .= $c;
501 72
                    break;
502 72
            }
503 73
        }
504
505 75
        return $prefix;
506
    }
507
508
    /**
509
     * Returns whether the glob contains a dynamic part.
510
     *
511
     * The glob contains a dynamic part if it contains an unescaped "*" or
512
     * "{" character.
513
     *
514
     * @param string $glob The glob to test.
515
     *
516
     * @return bool Returns `true` if the glob contains a dynamic part and
517
     *              `false` otherwise.
518
     */
519 40
    public static function isDynamic($glob)
520
    {
521 40
        return false !== strpos($glob, '*') || false !== strpos($glob, '{') || false !== strpos($glob, '?') || false !== strpos($glob, '[');
522
    }
523
524
    private function __construct()
525
    {
526
    }
527
}
528