Passed
Pull Request — master (#7)
by Roman
01:21
created

FileHelper   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 711
Duplicated Lines 0 %

Test Coverage

Coverage 79.72%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 204
dl 0
loc 711
ccs 173
cts 217
cp 0.7972
rs 2
c 3
b 0
f 0
wmc 113

23 Methods

Rating   Name   Duplication   Size   Complexity  
C matchPathname() 0 45 12
A setBasePath() 0 9 2
A isSelfDirectory() 0 4 3
C normalizePath() 0 31 14
A setCaseSensitive() 0 7 2
B copyDirectory() 0 38 10
A setExcept() 0 11 4
A matchBasename() 0 21 6
A normalizeOptions() 0 7 1
A isCaseInsensitive() 0 7 2
A unlink() 0 13 4
A parseExcludePattern() 0 33 5
A isPatternEndsWith() 0 7 3
A chmod() 0 9 2
A createDirectory() 0 17 4
A firstWildcardInPattern() 0 11 4
A filterPath() 0 16 5
A isPatternNoDir() 0 7 2
A setDestination() 0 10 4
B removeDirectory() 0 26 9
A isSourceDirectory() 0 9 2
A setOnly() 0 11 4
B lastExcludeMatchingFromList() 0 30 9

How to fix   Complexity   

Complex Class

Complex classes like FileHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileHelper, and based on these observations, apply Extract Interface, too.

1
<?php
2
declare(strict_types = 1);
3
4
namespace Yiisoft\Files;
5
6
use Yiisoft\Strings\StringHelper;
7
8
/**
9
 * FileHelper provides useful methods to manage files and directories
10
 */
11
class FileHelper
12
{
13
    /**
14
     * @var int PATTERN_NO_DIR
15
     */
16
    private const PATTERN_NO_DIR = 1;
17
18
    /**
19
     * @var int PATTERN_ENDS_WITH
20
     */
21
    private const PATTERN_ENDS_WITH = 4;
22
23
    /**
24
     * @var int PATTERN_MUST_BE_DIR
25
     */
26
    private const PATTERN_MUST_BE_DIR = 8;
27
28
    /**
29
     * @var int PATTERN_NEGATIVE
30
     */
31
    private const PATTERN_NEGATIVE = 16;
32
33
    /**
34
     * @var int PATTERN_CASE_INSENSITIVE
35
     */
36
    private const PATTERN_CASE_INSENSITIVE = 32;
37
38
    /**
39
     * Creates a new directory.
40
     *
41
     * This method is similar to the PHP `mkdir()` function except that it uses `chmod()` to set the permission of the
42
     * created directory in order to avoid the impact of the `umask` setting.
43
     *
44
     * @param string $path path of the directory to be created.
45
     * @param int $mode the permission to be set for the created directory.
46
     *
47
     * @return bool whether the directory is created successfully.
48
     */
49 17
    public static function createDirectory(string $path, int $mode = 0775): bool
50
    {
51 17
        $path = static::normalizePath($path);
52
53
        try {
54 17
            return \mkdir($path, $mode, \true) && \is_dir($path);
55 1
        } catch (\Exception $e) {
56 1
            if (!is_dir($path)) {
57
                throw new \RuntimeException(
58
                    "Failed to create directory \"$path\": " . $e->getMessage(),
59
                    $e->getCode(),
60
                    $e
61
                );
62
            }
63
        }
64
65 1
        return static::chmod($path, $mode);
66
    }
67
68
    /**
69
     * Set permissions directory.
70
     *
71
     * @param string $path
72
     * @param integer $mode
73
     *
74
     * @throws \RuntimeException
75
     *
76
     * @return boolean|null
77
     */
78 2
    private static function chmod(string $path, int $mode): ?bool
79
    {
80
        try {
81 2
            return chmod($path, $mode);
82
        } catch (\Exception $e) {
83
            throw new \RuntimeException(
84
                "Failed to change permissions for directory \"$path\": " . $e->getMessage(),
85
                $e->getCode(),
86
                $e
87
            );
88
        }
89
    }
90
91
    /**
92
     * Normalizes a file/directory path.
93
     *
94
     * The normalization does the following work:
95
     *
96
     * - Convert all directory separators into `/` (e.g. "\a/b\c" becomes "/a/b/c")
97
     * - Remove trailing directory separators (e.g. "/a/b/c/" becomes "/a/b/c")
98
     * - Turn multiple consecutive slashes into a single one (e.g. "/a///b/c" becomes "/a/b/c")
99
     * - Remove ".." and "." based on their meanings (e.g. "/a/./b/../c" becomes "/a/c")
100
     *
101
     * @param string $path the file/directory path to be normalized
102
     *
103
     * @return string the normalized file/directory path
104
     */
105 17
    public static function normalizePath(string $path): string
106
    {
107 17
        $isWindowsShare = strpos($path, '\\\\') === 0;
108
109 17
        if ($isWindowsShare) {
110 1
            $path = substr($path, 2);
111
        }
112
113 17
        $path = rtrim(strtr($path, '/\\', '//'), '/');
114
115 17
        if (strpos('/' . $path, '/.') === false && strpos($path, '//') === false) {
116 17
            return $isWindowsShare ? "\\\\$path" : $path;
117
        }
118
119 1
        $parts = [];
120
121 1
        foreach (explode('/', $path) as $part) {
122 1
            if ($part === '..' && !empty($parts) && end($parts) !== '..') {
123 1
                array_pop($parts);
124 1
            } elseif ($part !== '.' && ($part !== '' || empty($parts))) {
125 1
                $parts[] = $part;
126
            }
127
        }
128
129 1
        $path = implode('/', $parts);
130
131 1
        if ($isWindowsShare) {
132 1
            $path = '\\\\' . $path;
133
        }
134
135 1
        return $path === '' ? '.' : $path;
136
    }
137
138
    /**
139
     * Removes a directory (and all its content) recursively.
140
     *
141
     * @param string $directory the directory to be deleted recursively.
142
     * @param array $options options for directory remove. Valid options are:
143
     *
144
     * - traverseSymlinks: boolean, whether symlinks to the directories should be traversed too.
145
     *   Defaults to `false`, meaning the content of the symlinked directory would not be deleted.
146
     *   Only symlink would be removed in that default case.
147
     *
148
     * @return void
149
     */
150 17
    public static function removeDirectory(string $directory, array $options = []): void
151
    {
152 17
        if (!empty($options['traverseSymlinks']) || !is_link($directory)) {
153 17
            if (!($handle = @opendir($directory))) {
154 1
                return;
155
            }
156
157 17
            while (($file = readdir($handle)) !== false) {
158 17
                if ($file === '.' || $file === '..') {
159 17
                    continue;
160
                }
161 16
                $path = $directory . '/' . $file;
162 16
                if (is_dir($path)) {
163 16
                    self::removeDirectory($path, $options);
164
                } else {
165 10
                    self::unlink($path);
166
                }
167
            }
168
169 17
            closedir($handle);
170
        }
171
172 17
        if (is_link($directory)) {
173 2
            self::unlink($directory);
174
        } else {
175 17
            rmdir($directory);
176
        }
177 17
    }
178
179
    /**
180
     * Removes a file or symlink in a cross-platform way.
181
     *
182
     * @param string $path
183
     *
184
     * @return bool
185
     */
186 10
    public static function unlink(string $path): bool
187
    {
188 10
        $isWindows = DIRECTORY_SEPARATOR === '\\';
189
190 10
        if (!$isWindows) {
191 10
            return unlink($path);
192
        }
193
194
        if (is_link($path) && is_dir($path)) {
195
            return rmdir($path);
196
        }
197
198
        return unlink($path);
199
    }
200
201
    /**
202
     * Copies a whole directory as another one.
203
     *
204
     * The files and sub-directories will also be copied over.
205
     *
206
     * @param string $source the source directory.
207
     * @param string $destination the destination directory.
208
     * @param array $options options for directory copy. Valid options are:
209
     *
210
     * - dirMode: integer, the permission to be set for newly copied directories. Defaults to 0775.
211
     * - fileMode:  integer, the permission to be set for newly copied files. Defaults to the current environment
212
     *   setting.
213
     * - filter: callback, a PHP callback that is called for each directory or file.
214
     *   The signature of the callback should be: `function ($path)`, where `$path` refers the full path to be filtered.
215
     *   The callback can return one of the following values:
216
     *
217
     *   * true: the directory or file will be copied (the "only" and "except" options will be ignored).
218
     *   * false: the directory or file will NOT be copied (the "only" and "except" options will be ignored).
219
     *   * null: the "only" and "except" options will determine whether the directory or file should be copied.
220
     *
221
     * - only: array, list of patterns that the file paths should match if they want to be copied. A path matches a
222
     *   pattern if it contains the pattern string at its end. For example, '.php' matches all file paths ending with
223
     *   '.php'.
224
     *   Note, the '/' characters in a pattern matches both '/' and '\' in the paths. If a file path matches a pattern
225
     *   in both "only" and "except", it will NOT be copied.
226
     * - except: array, list of patterns that the files or directories should match if they want to be excluded from
227
     *   being copied. A path matches a pattern if it contains the pattern string at its end. Patterns ending with '/'
228
     *   apply to directory paths only, and patterns not ending with '/' apply to file paths only. For example, '/a/b'
229
     *   matches all file paths ending with '/a/b'; and '.svn/' matches directory paths ending with '.svn'. Note, the
230
     *   '/' characters in a pattern matches both '/' and '\' in the paths.
231
     * - caseSensitive: boolean, whether patterns specified at "only" or "except" should be case sensitive. Defaults to
232
     *   true.
233
     * - recursive: boolean, whether the files under the subdirectories should also be copied. Defaults to true.
234
     * - beforeCopy: callback, a PHP callback that is called before copying each sub-directory or file. If the callback
235
     *   returns false, the copy operation for the sub-directory or file will be cancelled. The signature of the
236
     *   callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file to be copied from,
237
     *   while `$to` is the copy target.
238
     * - afterCopy: callback, a PHP callback that is called after each sub-directory or file is successfully copied.
239
     *   The signature of the callback should be: `function ($from, $to)`, where `$from` is the sub-directory or file
240
     *   copied from, while `$to` is the copy target.
241
     * - copyEmptyDirectories: boolean, whether to copy empty directories. Set this to false to avoid creating
242
     *   directories that do not contain files. This affects directories that do not contain files initially as well as
243
     *   directories that do not contain files at the target destination because files have been filtered via `only` or
244
     *   `except`. Defaults to true.
245
     *
246
     * @throws \InvalidArgumentException if unable to open directory
247
     * @throws \Exception
248
     *
249
     * @return void
250
     */
251 11
    public static function copyDirectory(string $source, string $destination, array $options = []): void
252
    {
253 11
        $source = static::normalizePath($source);
254 11
        $destination = static::normalizePath($destination);
255
256 11
        static::isSelfDirectory($source, $destination);
257
258 9
        $destinationExists = static::setDestination($destination, $options);
259
260 9
        $handle = static::isSourceDirectory($source);
261
262 9
        $options = static::setBasePath($source, $options);
263
264 9
        while (($file = readdir($handle)) !== false) {
265 9
            if ($file === '.' || $file === '..') {
266 9
                continue;
267
            }
268
269 7
            $from = $source . '/' . $file;
270 7
            $to = $destination . '/' . $file;
271
272 7
            if (static::filterPath($from, $options)) {
273 7
                if (is_file($from)) {
274 7
                    if (!$destinationExists) {
275 2
                        static::createDirectory($destination, $options['dirMode'] ?? 0775);
276 2
                        $destinationExists = true;
277
                    }
278 7
                    copy($from, $to);
279 7
                    if (isset($options['fileMode'])) {
280 7
                        static::chmod($to, $options['fileMode']);
281
                    }
282 6
                } elseif (!isset($options['recursive']) || $options['recursive']) {
283 5
                    static::copyDirectory($from, $to, $options);
284
                }
285
            }
286
        }
287
288 9
        closedir($handle);
289 9
    }
290
291
    /**
292
     * Check copy it self directory.
293
     *
294
     * @param string $source
295
     * @param string $destination
296
     *
297
     * @throws \InvalidArgumentException
298
     *
299
     * @return boolean
300
     */
301 11
    private static function isSelfDirectory(string $source, string $destination)
302
    {
303 11
        if ($source === $destination || strpos($destination, $source . '/') === 0) {
304 2
            throw new \InvalidArgumentException('Trying to copy a directory to itself or a subdirectory.');
305
        }
306 9
    }
307
308
    /**
309
     * Check if exist source directory.
310
     *
311
     * @param string $source
312
     *
313
     * @throws \InvalidArgumentException
314
     *
315
     * @return resource
316
     */
317 9
    private static function isSourceDirectory(string $source)
318
    {
319 9
        $handle = @opendir($source);
320
321 9
        if ($handle === false) {
322
            throw new \InvalidArgumentException("Unable to open directory: $source");
323
        }
324
325 9
        return $handle;
326
    }
327
328
    /**
329
     * Set base path directory.
330
     *
331
     * @param string $source
332
     * @param array $options
333
     *
334
     * @return array
335
     */
336 9
    private static function setBasePath(string $source, array $options): array
337
    {
338 9
        if (!isset($options['basePath'])) {
339
            // this should be done only once
340 9
            $options['basePath'] = realpath($source);
341 9
            $options = static::normalizeOptions($options);
342
        }
343
344 9
        return $options;
345
    }
346
347
    /**
348
     * Set destination directory.
349
     *
350
     * @param string $destination
351
     * @param array $options
352
     *
353
     * @return bool
354
     */
355 9
    private static function setDestination(string $destination, array $options): bool
356
    {
357 9
        $destinationExists = is_dir($destination);
358
359 9
        if (!$destinationExists && (!isset($options['copyEmptyDirectories']) || $options['copyEmptyDirectories'])) {
360 5
            static::createDirectory($destination, $options['dirMode'] ?? 0775);
361 5
            $destinationExists = true;
362
        }
363
364 9
        return $destinationExists;
365
    }
366
367
    /**
368
     * Normalize options.
369
     *
370
     * @param array $options raw options.
371
     *
372
     * @return array normalized options.
373
     */
374 9
    protected static function normalizeOptions(array $options): array
375
    {
376 9
        $options = static::setCaseSensitive($options);
377 9
        $options = static::setExcept($options);
378 9
        $options = static::setOnly($options);
379
380 9
        return $options;
381
    }
382
383
    /**
384
     * Set options case sensitive.
385
     *
386
     * @param array $options
387
     *
388
     * @return array
389
     */
390 9
    private static function setCaseSensitive(array $options): array
391
    {
392 9
        if (!array_key_exists('caseSensitive', $options)) {
393 9
            $options['caseSensitive'] = true;
394
        }
395
396 9
        return $options;
397
    }
398
399
    /**
400
     * Set options except.
401
     *
402
     * @param array $options
403
     *
404
     * @return array
405
     */
406 9
    private static function setExcept(array $options): array
407
    {
408 9
        if (isset($options['except'])) {
409 1
            foreach ($options['except'] as $key => $value) {
410 1
                if (\is_string($value)) {
411 1
                    $options['except'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
412
                }
413
            }
414
        }
415
416 9
        return $options;
417
    }
418
419
    /**
420
     * Set options only.
421
     *
422
     * @param array $options
423
     *
424
     * @return array
425
     */
426 9
    private static function setOnly(array $options): array
427
    {
428 9
        if (isset($options['only'])) {
429 2
            foreach ($options['only'] as $key => $value) {
430 2
                if (\is_string($value)) {
431 2
                    $options['only'][$key] = self::parseExcludePattern($value, $options['caseSensitive']);
432
                }
433
            }
434
        }
435
436 9
        return $options;
437
    }
438
439
    /**
440
     * Checks if the given file path satisfies the filtering options.
441
     *
442
     * @param string $path the path of the file or directory to be checked.
443
     * @param array $options the filtering options.
444
     *
445
     * @return bool whether the file or directory satisfies the filtering options.
446
     */
447 7
    public static function filterPath(string $path, array $options): bool
448
    {
449 7
        $path = str_replace('\\', '/', $path);
450
451 7
        if (!empty($options['except'])) {
452 1
            if ((self::lastExcludeMatchingFromList($options['basePath'], $path, $options['except'])) !== null) {
453 1
                return false;
454
            }
455
        }
456
457 7
        if (!empty($options['only']) && !is_dir($path)) {
458
            // don't check PATTERN_NEGATIVE since those entries are not prefixed with !
459 2
            return self::lastExcludeMatchingFromList($options['basePath'], $path, $options['only']) !== null;
460
        }
461
462 7
        return true;
463
    }
464
465
    /**
466
     * Searches for the first wildcard character in the pattern.
467
     *
468
     * @param string $pattern the pattern to search in.
469
     *
470
     * @return int|bool position of first wildcard character or false if not found.
471
     */
472 2
    private static function firstWildcardInPattern(string $pattern)
473
    {
474 2
        $wildcards = ['*', '?', '[', '\\'];
475
        $wildcardSearch = function ($carry, $item) use ($pattern) {
476 2
            $position = strpos($pattern, $item);
477 2
            if ($position === false) {
478 2
                return $carry === false ? $position : $carry;
479
            }
480 2
            return $carry === false ? $position : min($carry, $position);
481 2
        };
482 2
        return array_reduce($wildcards, $wildcardSearch, false);
483
    }
484
485
486
    /**
487
     * Scan the given exclude list in reverse to see whether pathname should be ignored.
488
     *
489
     * The first match (i.e. the last on the list), if any, determines the fate.  Returns the element which matched,
490
     * or null for undecided.
491
     *
492
     * Based on last_exclude_matching_from_list() from dir.c of git 1.8.5.3 sources.
493
     *
494
     * @param string $basePath.
495
     * @param string $path.
496
     * @param array $excludes list of patterns to match $path against.
497
     *
498
     * @return null|array null or one of $excludes item as an array with keys: 'pattern', 'flags'.
499
     *
500
     * @throws \InvalidArgumentException if any of the exclude patterns is not a string or an array with keys: pattern,
501
     *                                   flags, firstWildcard.
502
     */
503 2
    private static function lastExcludeMatchingFromList(string $basePath, string $path, array $excludes): ?array
504
    {
505 2
        foreach (array_reverse($excludes) as $exclude) {
506 2
            if (\is_string($exclude)) {
507
                $exclude = self::parseExcludePattern($exclude, false);
508
            }
509
510 2
            if (!isset($exclude['pattern'], $exclude['flags'], $exclude['firstWildcard'])) {
511
                throw new \InvalidArgumentException(
512
                    'If exclude/include pattern is an array it must contain the pattern, flags and firstWildcard keys.'
513
                );
514
            }
515
516 2
            if (($exclude['flags'] & self::PATTERN_MUST_BE_DIR) && !is_dir($path)) {
517
                continue;
518
            }
519
520 2
            if ($exclude['flags'] & self::PATTERN_NO_DIR) {
521
                if (self::matchBasename(basename($path), $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
522
                    return $exclude;
523
                }
524
                continue;
525
            }
526
527 2
            if (self::matchPathname($path, $basePath, $exclude['pattern'], $exclude['firstWildcard'], $exclude['flags'])) {
528 2
                return $exclude;
529
            }
530
        }
531
532 2
        return null;
533
    }
534
535
    /**
536
     * Performs a simple comparison of file or directory names.
537
     *
538
     * Based on match_basename() from dir.c of git 1.8.5.3 sources.
539
     *
540
     * @param string $baseName file or directory name to compare with the pattern.
541
     * @param string $pattern the pattern that $baseName will be compared against.
542
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern.
543
     * @param int $flags pattern flags
544
     *
545
     * @return bool whether the name matches against pattern
546
     */
547
    private static function matchBasename(string $baseName, string $pattern, $firstWildcard, int $flags): bool
548
    {
549
        if ($firstWildcard === false) {
550
            if ($pattern === $baseName) {
551
                return true;
552
            }
553
        } elseif ($flags & self::PATTERN_ENDS_WITH) {
554
            /* "*literal" matching against "fooliteral" */
555
            $n = StringHelper::byteLength($pattern);
556
            if (StringHelper::byteSubstr($pattern, 1, $n) === StringHelper::byteSubstr($baseName, -$n, $n)) {
557
                return true;
558
            }
559
        }
560
561
        $matchOptions = [];
562
563
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
564
            $matchOptions['caseSensitive'] = false;
565
        }
566
567
        return StringHelper::matchWildcard($pattern, $baseName, $matchOptions);
568
    }
569
570
    /**
571
     * Compares a path part against a pattern with optional wildcards.
572
     *
573
     * Based on match_pathname() from dir.c of git 1.8.5.3 sources.
574
     *
575
     * @param string $path full path to compare
576
     * @param string $basePath base of path that will not be compared
577
     * @param string $pattern the pattern that path part will be compared against
578
     * @param int|bool $firstWildcard location of first wildcard character in the $pattern
579
     * @param int $flags pattern flags
580
     *
581
     * @return bool whether the path part matches against pattern
582
     */
583 2
    private static function matchPathname(string $path, string $basePath, string $pattern, $firstWildcard, int $flags): bool
584
    {
585
        // match with FNM_PATHNAME; the pattern has base implicitly in front of it.
586 2
        if (strpos($pattern, '/') === 0) {
587
            $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
588
            if ($firstWildcard !== false && $firstWildcard !== 0) {
589
                $firstWildcard--;
590
            }
591
        }
592
593 2
        $namelen = StringHelper::byteLength($path) - (empty($basePath) ? 0 : StringHelper::byteLength($basePath) + 1);
594 2
        $name = StringHelper::byteSubstr($path, -$namelen, $namelen);
595
596 2
        if ($firstWildcard !== 0) {
597 2
            if ($firstWildcard === false) {
598 1
                $firstWildcard = StringHelper::byteLength($pattern);
599
            }
600
601
            // if the non-wildcard part is longer than the remaining pathname, surely it cannot match.
602 2
            if ($firstWildcard > $namelen) {
603 1
                return false;
604
            }
605
606 2
            if (strncmp($pattern, $name, (int) $firstWildcard)) {
607 2
                return false;
608
            }
609
610 2
            $pattern = StringHelper::byteSubstr($pattern, (int) $firstWildcard, StringHelper::byteLength($pattern));
611 2
            $name = StringHelper::byteSubstr($name, (int) $firstWildcard, $namelen);
612
613
            // If the whole pattern did not have a wildcard, then our prefix match is all we need; we do not need to call fnmatch at all.
614 2
            if (empty($pattern) && empty($name)) {
615 1
                return true;
616
            }
617
        }
618
619
        $matchOptions = [
620 2
            'filePath' => true
621
        ];
622
623 2
        if ($flags & self::PATTERN_CASE_INSENSITIVE) {
624
            $matchOptions['caseSensitive'] = false;
625
        }
626
627 2
        return StringHelper::matchWildcard($pattern, $name, $matchOptions);
628
    }
629
630
    /**
631
     * Processes the pattern, stripping special characters like / and ! from the beginning and settings flags instead.
632
     *
633
     * @param string $pattern
634
     * @param bool $caseSensitive
635
     *
636
     * @return array with keys: (string) pattern, (int) flags, (int|bool) firstWildcard
637
     */
638 2
    private static function parseExcludePattern(string $pattern, bool $caseSensitive): array
639
    {
640
        $result = [
641 2
            'pattern' => $pattern,
642 2
            'flags' => 0,
643
            'firstWildcard' => false,
644
        ];
645
646 2
        $result = static::isCaseInsensitive($caseSensitive, $result);
647
648 2
        if (!isset($pattern[0])) {
649
            return $result;
650
        }
651
652 2
        if (strpos($pattern, '!') === 0) {
653
            $result['flags'] |= self::PATTERN_NEGATIVE;
654
            $pattern = StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern));
655
        }
656
657 2
        if (StringHelper::byteLength($pattern) && StringHelper::byteSubstr($pattern, -1, 1) === '/') {
658
            $pattern = StringHelper::byteSubstr($pattern, 0, -1);
659
            $result['flags'] |= self::PATTERN_MUST_BE_DIR;
660
        }
661
662 2
        $result = static::isPatternNoDir($pattern, $result);
663
664 2
        $result['firstWildcard'] = self::firstWildcardInPattern($pattern);
665
666 2
        $result = static::isPatternEndsWith($pattern, $result);
667
668 2
        $result['pattern'] = $pattern;
669
670 2
        return $result;
671
    }
672
673
    /**
674
     * Check isCaseInsensitive.
675
     *
676
     * @param boolean $caseSensitive
677
     * @param array $result
678
     *
679
     * @return array
680
     */
681 2
    private static function isCaseInsensitive(bool $caseSensitive, array $result): array
682
    {
683 2
        if (!$caseSensitive) {
684
            $result['flags'] |= self::PATTERN_CASE_INSENSITIVE;
685
        }
686
687 2
        return $result;
688
    }
689
690
    /**
691
     * Check pattern no directory.
692
     *
693
     * @param string $pattern
694
     * @param array $result
695
     *
696
     * @return array
697
     */
698 2
    private static function isPatternNoDir(string $pattern, array $result): array
699
    {
700 2
        if (strpos($pattern, '/') === false) {
701
            $result['flags'] |= self::PATTERN_NO_DIR;
702
        }
703
704 2
        return $result;
705
    }
706
707
    /**
708
     * Check pattern ends with
709
     *
710
     * @param string $pattern
711
     * @param array $result
712
     *
713
     * @return array
714
     */
715 2
    private static function isPatternEndsWith(string $pattern, array $result): array
716
    {
717 2
        if (strpos($pattern, '*') === 0 && self::firstWildcardInPattern(StringHelper::byteSubstr($pattern, 1, StringHelper::byteLength($pattern))) === false) {
718
            $result['flags'] |= self::PATTERN_ENDS_WITH;
719
        }
720
721 2
        return $result;
722
    }
723
}
724