MonitorConfigurator   F
last analyzed

Complexity

Total Complexity 76

Size/Duplication

Total Lines 603
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
wmc 76
lcom 1
cbo 1
dl 0
loc 603
rs 2.3169
c 0
b 0
f 0

19 Methods

Rating   Name   Duplication   Size   Complexity  
A factory() 0 4 1
B isPathMatchMonitor() 0 39 8
B isStringMatch() 0 34 10
C mayDirectoryContainMonitoredItems() 0 52 13
A getDirnamesWithoutEmpty() 0 10 4
A getBaseDirectory() 0 4 1
A getBaseDirectoryWithTrailingSlash() 0 4 2
A setBaseDirectory() 0 15 5
A getLevel() 0 4 1
A getEffectiveLevel() 0 4 1
A setLevel() 0 11 2
A getFilesToMonitor() 0 4 1
F setFilesToMonitor() 0 103 21
A isFireModifiedOnDirectories() 0 4 1
A setFireModifiedOnDirectories() 0 5 1
A isMonitorCreatedOnly() 0 4 1
A setMonitorCreatedOnly() 0 5 1
A isAutoCreateNotFoundMonitor() 0 4 1
A setAutoCreateNotFoundMonitor() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like MonitorConfigurator 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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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 MonitorConfigurator, and based on these observations, apply Extract Interface, too.

1
<?php
2
/*
3
 * This file is part of Dimsh\React\Filesystem\Monitor;
4
 *
5
 * (c) Abdulrahman Dimashki <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
12
namespace Dimsh\React\Filesystem\Monitor;
13
14
class MonitorConfigurator
15
{
16
    use MonitorTrait;
17
18
    /**
19
     * @var string
20
     */
21
    protected $base_directory = '';
22
23
    /**
24
     * @var int
25
     */
26
    protected $level = 1;
27
28
    /**
29
     * The nesting level to work on after analyzing the patterns in $files_to_monitor
30
     *
31
     * @internal
32
     * @var int
33
     */
34
    protected $effective_level = 0;
35
36
    /**
37
     * @var bool
38
     */
39
    protected $monitor_created_only = false;
40
41
    /**
42
     * @var string[]
43
     */
44
    protected $files_to_monitor = [];
45
46
    /**
47
     * @var bool
48
     */
49
    protected $auto_create_not_found_monitor = false;
50
51
    /**
52
     * Array where keys are the patterns set by $this->setFilesToMonitor() after applying
53
     * fixes to them by $this->fixPathsSlashes() (remove duplicated slashes and so)
54
     * The value for each is an array with the following keys:
55
     * 'end_with_slash'    : Is the pattern ends with a slash? then it is considered
56
     *                       a pattern for a directory which needs to be monitored
57
     * 'is_not_recursive'  : Is this pattern represents a specific level matcher (not recursive)
58
     *                       this happens when the pattern has at least one slash character in
59
     *                       the middle and in this case it will not be considered as a recursive
60
     *                       pattern.
61
     * 'path_level'        : The path level of the pattern (count of slashes excluding the leading
62
     *                       and the trailing, this should be 0 for recursive patterns and greater
63
     *                       than 0 for non recursive.
64
     * @internal
65
     * @var [][]
66
     */
67
    protected $analyzed_files_to_monitor = [];
68
69
    /**
70
     * Tell if the patterns passed to $this->setFilesToMonitor() has a pattern
71
     * with empty string which indicates the caller wants to monitor the
72
     * base directory for changes.
73
     *
74
     * @internal
75
     * @var bool
76
     */
77
    protected $files_to_monitor_has_empty_string = false;
78
79
    /**
80
     * This is for speed optimization, it indicates what all the patterns in
81
     * $this->analyzed_files_to_monitor has the key: 'is_not_recursive' as true.
82
     *
83
     * @internal
84
     * @var bool
85
     */
86
    protected $is_all_files_to_monitor_not_recursive = false;
87
88
    /**
89
     * an array of dirname() calls to each pattern in the keys of:
90
     * $this->analyzed_files_to_monitor -- recursively until we get
91
     * the empty string.
92
     *
93
     * This is used for performance optimization.
94
     *
95
     * All the entries are without leading slash.
96
     *
97
     * This array does not contain the empty string, and do not have duplicates.
98
     *
99
     * @internal
100
     * @var array
101
     */
102
    protected $dirnames_from_files_to_monitor = [];
103
104
    /**
105
     * The base directory string applied to preg_quote($base_dir, '@')
106
     *
107
     * @internal
108
     * @var string
109
     */
110
    protected $preg_quoted_base_directory = '';
111
112
    /**
113
     * Whether to fire the modified events on directories, default to false.
114
     *
115
     * @var bool
116
     */
117
    protected $fire_modified_on_directories = false;
118
119
120
    /**
121
     * @return MonitorConfigurator
122
     */
123
    public static function factory()
124
    {
125
        return new static();
126
    }
127
128
    /**
129
     * Tell if path (directory or file) is to be monitored by this config.
130
     *
131
     * Directories ends with slash.
132
     *
133
     * @param string $path
134
     *
135
     * @return bool
136
     */
137
    public function isPathMatchMonitor(string $path)
138
    {
139
        if (strpos($path, $this->base_directory) !== 0) {
140
            return false;
141
        }
142
        $trailing_slash = '';
143
        if ($this->hasTrailingSlash($path) && $path !== '/') {
144
            $trailing_slash = '/';
145
            $path           = rtrim($path, '/');
146
        }
147
        $remaining_path = preg_replace('@^' . $this->preg_quoted_base_directory . '@', '', $path);
148
        // Here $remaining_path is starting with slash (or be empty string)
149
        if ($remaining_path === '') {
150
            // this happens if $path = $this->getBaseDirecotry()
151
            // In this case the only pattern in $this->files_to_monitor array to match
152
            // is the empty string, which will be checked.
153
            return $this->files_to_monitor_has_empty_string;
154
        }
155
        /*
156
         * Here $remaining_path has a leading slash unless Base Directory is
157
         * the root '/' dir, And we have to fix the path level in that case
158
         */
159
        $path_level = substr_count($remaining_path, '/');
160
        if ($this->base_directory === '/') {
161
            $path_level++;
162
        }
163
        if ($this->effective_level > 0 && $path_level > $this->effective_level) {
164
            return false;
165
        }
166
        /*
167
         * Here $remaining_path has a leading slash (unless Base Directory is
168
         * the root '/' dir), which will cause troubles with patterns
169
         * because patterns are meant to be always relative to the base directory.
170
         *
171
         * Here we will remove the leading slash.
172
         */
173
        $remaining_path = ltrim($remaining_path, '/');
174
        return $this->isStringMatch($remaining_path . $trailing_slash);
175
    }
176
177
    /**
178
     * Match string against all patterns saved in $this->getFilesToMonitor()
179
     * array of patterns (or names)
180
     *
181
     * @param string $relative_path
182
     *
183
     * @return bool
184
     */
185
    protected function isStringMatch($relative_path)
186
    {
187
        /**
188
         * $relative_path will never be '/' if this method is called from $this->isPathMatchMonitor()
189
         */
190
        $has_trailing_slash               = $this->hasTrailingSlash($relative_path) && $relative_path !== '/';
191
        $relative_path_with_leading_slash = substr($relative_path, 0, 1) === '/' ?
192
          $relative_path : '/' . $relative_path;
193
        $basename_relative_path           = basename($relative_path) . ($has_trailing_slash ? '/' : '');
194
        foreach ($this->analyzed_files_to_monitor as $pattern => $properties) {
195
            if ($has_trailing_slash && !$properties['end_with_slash']) {
196
                /**
197
                 * if the passed path is a directory, then it matches only patterns which end
198
                 * with slash, not others.
199
                 */
200
                continue;
201
            }
202
            if ($properties['is_not_recursive']) {
203
                /**
204
                 * $pattern does not have a leading slash, and since it is not
205
                 * recursive, here we will ensure both the pattern and the $relative_path
206
                 * have leading slashes to better match the path level accurately.
207
                 */
208
                if (fnmatch('/' . $pattern, $relative_path_with_leading_slash, FNM_PATHNAME | FNM_PERIOD)) {
209
                    return true;
210
                }
211
            } else {
212
                if (fnmatch($pattern, $basename_relative_path)) {
213
                    return true;
214
                }
215
            }
216
        }
217
        return false;
218
    }
219
220
    /**
221
     * Tell if the passed directory may contain items that matches the patterns,
222
     * it may contain them if its nest level is less than the configured level
223
     *
224
     * @param string $directory
225
     *
226
     * @return bool
227
     */
228
    public function mayDirectoryContainMonitoredItems($directory)
229
    {
230
        if (strpos($directory, $this->base_directory) !== 0) {
231
            return false;
232
        }
233
        if ($this->effective_level == 0 || $directory === $this->base_directory) {
234
            return true;
235
        }
236
        if ($directory !== '/') {
237
            $directory = rtrim($directory, '/');
238
        }
239
        $remaining_path = preg_replace('@^' . $this->preg_quoted_base_directory . '@', '', $directory);
240
        /*
241
         * Here $remaining_path has a leading slash unless Base Directory is
242
         * the root '/' dir, and we have to fix the path level in that case.
243
         */
244
        $path_level = substr_count($remaining_path, '/');
245
        if ($this->base_directory === '/') {
246
            $path_level++;
247
        }
248
        if ($this->effective_level > 0 && $path_level >= $this->effective_level) {
249
            return false;
250
        }
251
        /*
252
         * Here I want to search for the false more, for performance reasons
253
         * it is better to report false early for directories which will not
254
         * have any matches.
255
         *
256
         * will add the leading slash to remaining path
257
         */
258
        if (substr($remaining_path, 0, 1) !== '/') {
259
            $remaining_path = '/' . $remaining_path;
260
        }
261
        if ($this->is_all_files_to_monitor_not_recursive) {
262
            $matched_dirname = false;
263
            /**
264
             * $dirname_pattern is without a leading slash, I will add it since
265
             * $remaining_path has it and I want to restrict the matching more.
266
             */
267
            foreach ($this->dirnames_from_files_to_monitor as $dirname_pattern) {
268
                if (fnmatch('/' . $dirname_pattern, $remaining_path, FNM_PATHNAME | FNM_PERIOD)) {
269
                    $matched_dirname = true;
270
                    break;
271
                }
272
            }
273
            if (!$matched_dirname) {
274
                // no dirname pattern will match this path, we return false!
275
                return false;
276
            }
277
        }
278
        return true;
279
    }
280
281
    /**
282
     * Get an array of dirnames called on $path recursively.
283
     *
284
     * @param string $path
285
     *
286
     * @return array
287
     */
288
    protected function getDirnamesWithoutEmpty(string $path): array
289
    {
290
        $dirnames = [];
291
        // dirname('hi') == dirname('hi/') == '.'
292
        while (($dirname = dirname($path)) && $dirname != '.' && $dirname != $path) {
293
            $dirnames[] = $dirname;
294
            $path       = $dirname;
295
        }
296
        return $dirnames;
297
    }
298
299
    ////////////////////////////////////
300
    // Getters and Setters come next  //
301
    ////////////////////////////////////
302
303
    /**
304
     * Base dir is always without trailing slash unless it is the root '/' directory.
305
     *
306
     * @return string
307
     */
308
    public function getBaseDirectory(): string
309
    {
310
        return $this->base_directory;
311
    }
312
313
    /**
314
     * Return Base dir with trailing slash.
315
     *
316
     * @return string
317
     */
318
    public function getBaseDirectoryWithTrailingSlash(): string
319
    {
320
        return $this->base_directory == '/' ? $this->base_directory : $this->base_directory . '/';
321
    }
322
323
    /**
324
     * Set Base dir, trailing slash is removed.
325
     *
326
     * The directory may exist or not, if it is a relative path, the current directory
327
     * is used to construct an absolute path.
328
     *
329
     * If the directory exists and is a file, an Exception is thrown.
330
     *
331
     * @param string $base_directory
332
     *
333
     * @return MonitorConfigurator
334
     *
335
     * @throws \Exception
336
     */
337
    public function setBaseDirectory(string $base_directory): MonitorConfigurator
338
    {
339
        if (substr($base_directory, 0, 1) !== '/') {
340
            $base_directory = getcwd() . '/' . $base_directory;
341
        }
342
        if (file_exists($base_directory) && !is_dir($base_directory)) {
343
            throw new \Exception("base directory [$base_directory] passed to MonitorConfigurator represents an existing non-directory, it must be an existing directory or not existing at all.");
344
        }
345
        $this->base_directory = $this->fixPathSlashes($base_directory);
346
        if ($this->base_directory !== '/') {
347
            $this->base_directory = rtrim($this->base_directory, '/');
348
        }
349
        $this->preg_quoted_base_directory = preg_quote($this->base_directory, '@');
350
        return $this;
351
    }
352
353
    /**
354
     * @return int
355
     */
356
    public function getLevel(): int
357
    {
358
        return $this->level;
359
    }
360
361
    /**
362
     * Report the effective level we operate at according to the analysis of the patterns.
363
     *
364
     * @return int
365
     */
366
    public function getEffectiveLevel(): int
367
    {
368
        return $this->effective_level;
369
    }
370
371
    /**
372
     * Set the deep level to look for files, 0 means all recursive, 1 means
373
     * only directly inside Base-Dir, 2 means directly inside Base-Dir and
374
     * directly inside any direct child-directories in it, and so on.
375
     *
376
     * @param int $level
377
     *
378
     * @return MonitorConfigurator
379
     */
380
    public function setLevel(int $level): MonitorConfigurator
381
    {
382
        $this->level = $level;
383
        if (!empty($this->files_to_monitor)) {
384
            /**
385
             * This is needed because we re-analyze the patterns depending on the new level
386
             */
387
            $this->setFilesToMonitor($this->files_to_monitor);
388
        }
389
        return $this;
390
    }
391
392
    /**
393
     * @return string[]
394
     */
395
    public function getFilesToMonitor(): array
396
    {
397
        return $this->files_to_monitor;
398
    }
399
400
    /**
401
     * Set an array of Shell-Pattern for files or directories to monitor,
402
     * directory pattern must end with trailing slash.
403
     *
404
     * These patterns are meant to be relative to the base directory set by
405
     * $this->setBaseDirectory(), but leading slash has special meaning: it
406
     * will cause the pattern not to be recursive.
407
     * Leading slash in any pattern item will be removed internally and will
408
     * set a flag that this pattern will has a directory level set.
409
     *
410
     * To match directories inside base directory, you can pass the path or
411
     * path pattern as an item in the array, like:
412
     * [
413
     *   'dir1/*.xml',
414
     *   '* /dir2/*.py', # space used because comment ending matched
415
     * ]
416
     * The previous two patterns are not recursive because they have a slash
417
     * inside the pattern and they are equal to the following:
418
     * [
419
     *   '/dir1/*.xml',
420
     *   '/* /dir2/*.py', # space used because comment ending matched
421
     * ]
422
     *
423
     *
424
     * Example:
425
     *  ['*.yaml', 'log/']
426
     * This will monitor any file with extension 'yaml' inside Base-Dir and
427
     * recursively up to level set by setLevel() for changes (create, modify,
428
     * delete) and will do the same for any directory who's basename is 'log'
429
     *
430
     * Passing something like:
431
     *
432
     * ['*'.'/dir2/*.py'] will not search recursively, but will be limited to level 3
433
     * inside base directory no more and no less, and it can match if the level set
434
     * is greater or equal to 3.
435
     *
436
     * Level is checked for all patterns those which has '/' (slash) or not.
437
     *
438
     *
439
     * @param string[] $files_to_monitor
440
     *
441
     * @return MonitorConfigurator
442
     */
443
    public function setFilesToMonitor(array $files_to_monitor): MonitorConfigurator
444
    {
445
        $this->files_to_monitor                      = $files_to_monitor;
446
        $this->analyzed_files_to_monitor             = [];
447
        $this->dirnames_from_files_to_monitor        = [];
448
        $this->files_to_monitor_has_empty_string     = false;
449
        $this->is_all_files_to_monitor_not_recursive = true;
450
        $path_levels_array                           = [];
451
452
        foreach ($this->files_to_monitor as $pattern) {
453
            $pattern           = $this->fixPathSlashes($pattern);
454
            $has_leading_slash = false;
455
            if ($pattern && substr($pattern, 0, 1) === '/') {
456
                // if $pattern = '/', then it will become the empty string which will match
457
                // the base directory only
458
                $pattern           = ltrim($pattern, '/');
459
                $has_leading_slash = true;
460
            }
461
            if ($pattern === '') {
462
                $this->files_to_monitor_has_empty_string = true;
463
            }
464
465
            if (!isset($this->analyzed_files_to_monitor[$pattern])) {
466
                $this->analyzed_files_to_monitor[$pattern] = [
467
                  'end_with_slash' => substr($pattern, -1, 1) === '/',
468
                ];
469
470
                $count_slashes = substr_count($pattern, '/');
471
472
                $this->analyzed_files_to_monitor[$pattern]['is_not_recursive'] = (
473
                  $count_slashes > 1 && $this->analyzed_files_to_monitor[$pattern]['end_with_slash']
474
                  ||
475
                  $count_slashes > 0 && !$this->analyzed_files_to_monitor[$pattern]['end_with_slash']
476
                  ||
477
                  $pattern === ''
478
                  ||
479
                  $has_leading_slash
480
                );
481
482
                $path_level = $count_slashes;
483
                if ($this->analyzed_files_to_monitor[$pattern]['end_with_slash']) {
484
                    $path_level--;
485
                }
486
                if ($path_level > 0) {
487
                    /*
488
                     * This happens if a pattern is like:
489
                     * 'dir/*.ext'
490
                     * 'dir/another_dir/'  (trailing slash for this is not counted as we decreased it already)
491
                     * '/dir1/dir2/f*.xml' (leading slash for this is not counted as we removed it already)
492
                     *
493
                     * In these cases and because there is a slash in the pattern, the path level must
494
                     * be increased by 1 because in the examples above:
495
                     * -the first line is targeting files at level 2
496
                     * -the second targeting a directory at level 2
497
                     * - the third targets files at level 3
498
                     */
499
                    $path_level++;
500
                }
501
                if ($this->level > 0 && $path_level > $this->level) {
502
                    /*
503
                     * We discard the whole pattern if the level of the pattern
504
                     * is not going to match.
505
                     * this is for speed optimization.
506
                     */
507
                    unset($this->analyzed_files_to_monitor[$pattern]);
508
                    continue;
509
                }
510
                $this->analyzed_files_to_monitor[$pattern]['path_level'] = $path_level;
511
                $path_levels_array[]                                     = $path_level;
512
513
                $this->is_all_files_to_monitor_not_recursive &= $this->analyzed_files_to_monitor[$pattern]['is_not_recursive'];
514
                $this->dirnames_from_files_to_monitor        = array_unique(
515
                    array_merge(
516
                        $this->dirnames_from_files_to_monitor,
517
                        $this->getDirnamesWithoutEmpty($pattern)
518
                    )
519
                );
520
            }
521
        }
522
523
        $this->effective_level = $this->level;
524
        if (!empty($path_levels_array)) {
525
            $this->effective_level = max($path_levels_array);
526
            if (0 === min($path_levels_array)) {
527
                $this->effective_level = 0;
528
            }
529
            if ($this->level !== 0) {
530
                if ($this->effective_level > $this->level) {
531
                    // this condition will never happen, just in case
532
                    $this->effective_level = $this->level;
533
                }
534
                if ($this->effective_level < $this->level && $this->effective_level === 0) {
535
                    $this->effective_level = $this->level;
536
                }
537
            }
538
            /*
539
             * Else: if $this->level is 0 (recursive)
540
             * Then $this->effective_level might be 0 which is what is set, or greater than 0 which is
541
             * good for performance
542
             */
543
        }
544
        return $this;
545
    }
546
547
    /**
548
     * @return bool
549
     */
550
    public function isFireModifiedOnDirectories(): bool
551
    {
552
        return $this->fire_modified_on_directories;
553
    }
554
555
    /**
556
     * Set to true to also fire the modified event on directories.
557
     *
558
     * @param bool $fire_modified_on_directories
559
     *
560
     * @return MonitorConfigurator
561
     */
562
    public function setFireModifiedOnDirectories(bool $fire_modified_on_directories): MonitorConfigurator
563
    {
564
        $this->fire_modified_on_directories = $fire_modified_on_directories;
565
        return $this;
566
    }
567
568
    /**
569
     * Is this monitor configuration to monitor the CREATED event only.
570
     *
571
     * @return bool
572
     */
573
    public function isMonitorCreatedOnly(): bool
574
    {
575
        return $this->monitor_created_only;
576
    }
577
578
    /**
579
     * Set a config to monitor the created event only, no modified and no delete.
580
     *
581
     * @param bool $monitor_created_only
582
     *
583
     * @return MonitorConfigurator
584
     */
585
    public function setMonitorCreatedOnly(bool $monitor_created_only): MonitorConfigurator
586
    {
587
        $this->monitor_created_only = $monitor_created_only;
588
        return $this;
589
    }
590
591
    /**
592
     * This instructs the monitor to create a child monitor when the base directory is not found.
593
     *
594
     * @return bool
595
     */
596
    public function isAutoCreateNotFoundMonitor(): bool
597
    {
598
        return $this->auto_create_not_found_monitor;
599
    }
600
601
    /**
602
     * Set to true to create another Monitor object which waits for the Base Directory to be created in case the
603
     * Base Directory does not exists.
604
     *
605
     * @see \Dimsh\React\Filesystem\Monitor\Monitor::__construct()
606
     *
607
     * @param bool $auto_create_not_found_monitor
608
     *
609
     * @return MonitorConfigurator
610
     */
611
    public function setAutoCreateNotFoundMonitor(bool $auto_create_not_found_monitor): MonitorConfigurator
612
    {
613
        $this->auto_create_not_found_monitor = $auto_create_not_found_monitor;
614
        return $this;
615
    }
616
}
617