Completed
Pull Request — master (#30)
by Christian
02:15
created

FileList   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 550
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 11

Test Coverage

Coverage 84.86%

Importance

Changes 0
Metric Value
wmc 52
lcom 1
cbo 11
dl 0
loc 550
ccs 157
cts 185
cp 0.8486
rs 7.9487
c 0
b 0
f 0

26 Methods

Rating   Name   Duplication   Size   Complexity  
A toArray() 0 13 2
A getIterator() 0 4 1
D getSourceIterator() 0 32 9
A count() 0 4 1
A __construct() 0 5 1
A name() 0 13 1
A notName() 0 13 1
A path() 0 13 1
A notPath() 0 13 1
A files() 0 12 1
A directories() 0 12 1
B depth() 0 37 6
A date() 0 12 1
A contains() 0 13 1
A notContains() 0 13 1
A filter() 0 7 1
A filterByClosure() 0 12 1
A sqlHasStatements() 0 10 2
B sqlSearchStatementsForTypes() 0 18 6
A sqlKeywordsToClasses() 0 7 1
A sqlWithDDL() 0 8 2
A sqlWithoutDDL() 0 8 2
A sqlWithDML() 0 8 2
A sqlWithoutDML() 0 8 2
A sqlWithTCL() 0 8 2
A sqlWithoutTCL() 0 8 2

How to fix   Complexity   

Complex Class

Complex classes like FileList 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 FileList, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace uuf6429\ElderBrother\Change;
4
5
use Symfony\Component\Finder\Comparator;
6
use Symfony\Component\Finder\Iterator;
7
use SqlParser\Parser as SqlParser;
8
use SqlParser\Statements;
9
10
class FileList implements \IteratorAggregate, \Countable
11
{
12
    /** @var string */
13
    protected $cacheKey;
14
15
    /** @var callable */
16
    protected $source;
17
18
    /** @var array */
19
    protected static $cache;
20
21
    /** @var \Iterator */
22
    protected $sourceResult;
23
24
    /**
25
     * @param string   $cacheKey Unique key to identify this collection of files
26
     * @param callable $source   Callable that return an iterator or array of FileInfo
27
     */
28 38
    public function __construct($cacheKey, callable $source)
29
    {
30 38
        $this->cacheKey = $cacheKey;
31 38
        $this->source = $source;
32 38
    }
33
34
    //region File / Path Name filtering
35
36
    /**
37
     * Search file names (excluding path) by pattern.
38
     *
39
     * @param string $pattern Pattern to look for in file name (regexp, glob, or string)
40
     *
41
     * @return static
42
     */
43 5
    public function name($pattern)
44
    {
45 5
        return new self(
46 5
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
47
            function () use ($pattern) {
48 5
                return new Iterator\FilenameFilterIterator(
49 5
                    $this->getSourceIterator(),
50 5
                    [$pattern],
51 5
                    []
52
                );
53 5
            }
54
        );
55
    }
56
57
    /**
58
     * Search file names (excluding path) by pattern.
59
     *
60
     * @param string $pattern Pattern to exclude files (regexp, glob, or string)
61
     *
62
     * @return static
63
     */
64 1
    public function notName($pattern)
65
    {
66 1
        return new self(
67 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
68
            function () use ($pattern) {
69 1
                return new Iterator\FilenameFilterIterator(
70 1
                    $this->getSourceIterator(),
71 1
                    [],
72 1
                    [$pattern]
73
                );
74 1
            }
75
        );
76
    }
77
78
    /**
79
     * Search path names by pattern.
80
     *
81
     * @param string $pattern Pattern to look for in path (regexp, glob, or string)
82
     *
83
     * @return static
84
     */
85 1
    public function path($pattern)
86
    {
87 1
        return new self(
88 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
89
            function () use ($pattern) {
90 1
                return new Iterator\PathFilterIterator(
91 1
                    $this->getSourceIterator(),
92 1
                    [$pattern],
93 1
                    []
94
                );
95 1
            }
96
        );
97
    }
98
99
    /**
100
     * Search path names by pattern.
101
     *
102
     * @param string $pattern Pattern to exclude paths (regexp, glob, or string)
103
     *
104
     * @return static
105
     */
106 1
    public function notPath($pattern)
107
    {
108 1
        return new self(
109 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
110
            function () use ($pattern) {
111 1
                return new Iterator\PathFilterIterator(
112 1
                    $this->getSourceIterator(),
113 1
                    [],
114 1
                    [$pattern]
115
                );
116 1
            }
117
        );
118
    }
119
120
    //endregion
121
122
    //region FS Item Type Filtering
123
124
    /**
125
     * Filters out anything that is not a file.
126
     *
127
     * @return static
128
     */
129 3
    public function files()
130
    {
131 3
        return new self(
132 3
            $this->cacheKey . '->' . __FUNCTION__ . '()',
133
            function () {
134 3
                return new Iterator\FileTypeFilterIterator(
135 3
                    $this->getSourceIterator(),
136 3
                    Iterator\FileTypeFilterIterator::ONLY_FILES
137
                );
138 3
            }
139
        );
140
    }
141
142
    /**
143
     * Filters out anything that is not a directory.
144
     *
145
     * @return static
146
     */
147 2
    public function directories()
148
    {
149 2
        return new self(
150 2
            $this->cacheKey . '->' . __FUNCTION__ . '()',
151
            function () {
152 2
                return new Iterator\FileTypeFilterIterator(
153 2
                    $this->getSourceIterator(),
154 2
                    Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES
155
                );
156 2
            }
157
        );
158
    }
159
160
    //endregion
161
162
    /**
163
     * Filters out items that do not match the specified level.
164
     *
165
     * @param string $level The depth expression (for example '< 1')
166
     *
167
     * @return static
168
     */
169 3
    public function depth($level)
170
    {
171 3
        $minDepth = 0;
172 3
        $maxDepth = PHP_INT_MAX;
173 3
        $comparator = new Comparator\NumberComparator($level);
174 3
        $comparatorTarget = intval($comparator->getTarget());
175
176 3
        switch ($comparator->getOperator()) {
177 3
            case '>':
178 1
                $minDepth = $comparatorTarget + 1;
179 1
                break;
180
181 2
            case '>=':
182
                $minDepth = $comparatorTarget;
183
                break;
184
185 2
            case '<':
186 2
                $maxDepth = $comparatorTarget - 1;
187 2
                break;
188
189
            case '<=':
190
                $maxDepth = $comparatorTarget;
191
                break;
192
193
            default:
194
                $minDepth = $maxDepth = $comparatorTarget;
195
                break;
196
        }
197
198 3
        return $this->filter(
199
            function (FileInfo $file) use ($minDepth, $maxDepth) {
200 3
                $depth = count(explode('/', str_replace('\\', '/', $file->getRelativePathname()))) - 1;
201
202 3
                return $depth >= $minDepth && $depth <= $maxDepth;
203 3
            }
204
        );
205
    }
206
207
    /**
208
     * Filters out items whose last modified do not match expression.
209
     *
210
     * @param string $date A date range string that can be parsed by `strtotime()``
211
     *
212
     * @return static
213
     */
214
    public function date($date)
215
    {
216
        return new self(
217
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $date . ')',
218
            function () use ($date) {
219
                return new Iterator\DateRangeFilterIterator(
220
                    $this->getSourceIterator(),
221
                    [new Comparator\DateComparator($date)]
222
                );
223
            }
224
        );
225
    }
226
227
    /**
228
     * Filters out files not matching a string or regexp.
229
     *
230
     * @param string $pattern A pattern (string or regexp)
231
     *
232
     * @return static
233
     */
234 2
    public function contains($pattern)
235
    {
236 2
        return new self(
237 2
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
238
            function () use ($pattern) {
239 2
                return new Iterator\FilecontentFilterIterator(
240 2
                    $this->getSourceIterator(),
241 2
                    [$pattern],
242 2
                    []
243
                );
244 2
            }
245
        );
246
    }
247
248
    /**
249
     * Filters out files matching a string or regexp.
250
     *
251
     * @param string $pattern A pattern (string or regexp)
252
     *
253
     * @return static
254
     */
255 1
    public function notContains($pattern)
256
    {
257 1
        return new self(
258 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
259
            function () use ($pattern) {
260 1
                return new Iterator\FilecontentFilterIterator(
261 1
                    $this->getSourceIterator(),
262 1
                    [],
263 1
                    [$pattern]
264
                );
265 1
            }
266
        );
267
    }
268
269
    /**
270
     * Filters using an anonymous function. Function receives a Change\FileInfo and must return false to filter it out.
271
     *
272
     * @param \Closure $closure An anonymous function
273
     *
274
     * @return static
275
     */
276 5
    public function filter(\Closure $closure)
277
    {
278 5
        return $this->filterByClosure(
279 5
            __FUNCTION__ . '(' . spl_object_hash($closure) . ')',
280
            $closure
281
        );
282
    }
283
284
    /**
285
     * Helper method that can be reused in more specific methods.
286
     *
287
     * @param string   $subKey  The cache sub key to use
288
     * @param \Closure $closure The filtering callback
289
     *
290
     * @return FileList
291
     */
292 9
    protected function filterByClosure($subKey, \Closure $closure)
293
    {
294 9
        return new self(
295 9
            $this->cacheKey . '->' . $subKey,
296
            function () use ($closure) {
297 8
                return new Iterator\CustomFilterIterator(
298 8
                    $this->getSourceIterator(),
299 8
                    [$closure]
300
                );
301 9
            }
302
        );
303
    }
304
305
    //region SQL filtering
306
307
    protected static $statementTypes = [
308
        'util' => [
309
            Statements\ExplainStatement::class,
310
            Statements\AnalyzeStatement::class,
311
            Statements\BackupStatement::class,
312
            Statements\CheckStatement::class,
313
            Statements\ChecksumStatement::class,
314
            Statements\OptimizeStatement::class,
315
            Statements\RepairStatement::class,
316
            Statements\RestoreStatement::class,
317
            Statements\SetStatement::class,
318
            Statements\ShowStatement::class,
319
        ],
320
        'ddl' => [
321
            Statements\AlterStatement::class,
322
            Statements\CreateStatement::class,
323
            Statements\DropStatement::class,
324
            Statements\RenameStatement::class,
325
            Statements\TruncateStatement::class,
326
        ],
327
        'dml' => [
328
            Statements\CallStatement::class,
329
            Statements\DeleteStatement::class,
330
            Statements\InsertStatement::class,
331
            Statements\ReplaceStatement::class,
332
            Statements\SelectStatement::class,
333
            Statements\UpdateStatement::class,
334
        ],
335
        'tcl' => [
336
            Statements\TransactionStatement::class,
337
        ],
338
    ];
339
340
    /**
341
     * Helper function to check if sql code has any of the specified statements.
342
     *
343
     * @param string[] $statementClasses The statement classes to look for
344
     * @param bool     $filterIn         True to "filter in", false to "filter out"
345
     *
346
     * @return FileList
347
     */
348 4
    protected function sqlHasStatements($statementClasses, $filterIn)
349
    {
350 4
        return $this->filterByClosure(
351 4
            __FUNCTION__ . '(' . implode(',', $statementClasses) . ')',
352
            function (FileInfo $file) use ($statementClasses, $filterIn) {
353 3
                return $this->sqlSearchStatementsForTypes($file->getSqlParser()->statements, $statementClasses)
354 3
                    ? $filterIn : !$filterIn;
355 4
            }
356
        );
357
    }
358
359
    /**
360
     * @param \SqlParser\Statement[] $statements
361
     * @param string[] $classes
362
     * @return boolean
363
     */
364 3
    protected function sqlSearchStatementsForTypes($statements, $classes)
365
    {
366 3
        foreach ($statements as $statement) {
367 3
            foreach ($classes as $class) {
368 3
                if ($statement instanceof $class) {
369 3
                    return true;
370
                }
371
            }
372
373 3
            if ($statement instanceof Statements\TransactionStatement) {
374 2
                if ($this->sqlSearchStatementsForTypes($statement->statements, $classes)) {
375 3
                    return true;
376
                }
377
            }
378
        }
379
380 3
        return false;
381
    }
382
383
    /**
384
     * @param string[] $keywords
385
     * @return string[]
386
     */
387
    protected function sqlKeywordsToClasses($keywords)
388
    {
389
        $keywords = array_map('strtoupper', $keywords);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $keywords. This often makes code more readable.
Loading history...
390
        $statements = array_replace(array_flip($keywords), SqlParser::$STATEMENT_PARSERS);
391
392
        return array_filter(array_unique($statements));
393
    }
394
395
    /**
396
     * @param string[]|null $keywords
397
     *
398
     * @return FileList
399
     */
400 2
    public function sqlWithDDL($keywords = null)
401
    {
402 2
        $classes = is_null($keywords)
403 2
            ? static::$statementTypes['ddl']
404 2
            : $this->sqlKeywordsToClasses($keywords);
405
406 2
        return $this->sqlHasStatements($classes, true);
407
    }
408
409
    /**
410
     * @param string[]|null $keywords
411
     *
412
     * @return FileList
413
     */
414 1
    public function sqlWithoutDDL($keywords = null)
415
    {
416 1
        $classes = is_null($keywords)
417 1
            ? static::$statementTypes['ddl']
418 1
            : $this->sqlKeywordsToClasses($keywords);
419
420 1
        return $this->sqlHasStatements($classes, false);
421
    }
422
423
    /**
424
     * @param string[]|null $keywords
425
     *
426
     * @return FileList
427
     */
428 1
    public function sqlWithDML($keywords = null)
429
    {
430 1
        $classes = is_null($keywords)
431 1
            ? static::$statementTypes['dml']
432 1
            : $this->sqlKeywordsToClasses($keywords);
433
434 1
        return $this->sqlHasStatements($classes, true);
435
    }
436
437
    /**
438
     * @param string[]|null $keywords
439
     *
440
     * @return FileList
441
     */
442 1
    public function sqlWithoutDML($keywords = null)
443
    {
444 1
        $classes = is_null($keywords)
445 1
            ? static::$statementTypes['dml']
446 1
            : $this->sqlKeywordsToClasses($keywords);
447
448 1
        return $this->sqlHasStatements($classes, false);
449
    }
450
451
    /**
452
     * @param string[]|null $keywords
453
     *
454
     * @return FileList
455
     */
456 1
    public function sqlWithTCL($keywords = null)
457
    {
458 1
        $classes = is_null($keywords)
459 1
            ? static::$statementTypes['tcl']
460 1
            : $this->sqlKeywordsToClasses($keywords);
461
462 1
        return $this->sqlHasStatements($classes, true);
463
    }
464
465
    /**
466
     * @param string[]|null $keywords
467
     *
468
     * @return FileList
469
     */
470
    public function sqlWithoutTCL($keywords = null)
471
    {
472
        $classes = is_null($keywords)
473
            ? static::$statementTypes['tcl']
474
            : $this->sqlKeywordsToClasses($keywords);
475
476
        return $this->sqlHasStatements($classes, false);
477
    }
478
479
    // TODO sqlTable()
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
480
    // TODO sqlNotTable()
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
481
    // TODO sqlFilter()
0 ignored issues
show
Coding Style Best Practice introduced by
Comments for TODO tasks are often forgotten in the code; it might be better to use a dedicated issue tracker.
Loading history...
482
483
    //endregion
484
485
    //region Iterator / Interface implementations
486
487
    /**
488
     * Returns array of file paths.
489
     *
490
     * @return string[]
491
     */
492 37
    public function toArray()
493
    {
494 37
        if (!isset(self::$cache[$this->cacheKey])) {
495 36
            self::$cache[$this->cacheKey] = array_map(
496 36
                function (FileInfo $file) {
497 31
                    return $file->getPathname();
498 36
                },
499 36
                array_values(iterator_to_array($this->getSourceIterator()))
500
            );
501
        }
502
503 37
        return self::$cache[$this->cacheKey];
504
    }
505
506
    /**
507
     * @return \Iterator
508
     */
509
    public function getIterator()
510
    {
511
        return new \ArrayIterator($this->toArray());
512
    }
513
514
    /**
515
     * @return \Iterator
516
     */
517 36
    public function getSourceIterator()
518
    {
519 36
        if (!$this->sourceResult) {
520 36
            $source = $this->source;
521 36
            $result = $source();
522
523 36
            if ($result instanceof \IteratorAggregate) {
524
                $this->sourceResult = $result->getIterator();
525
            } elseif ($result instanceof \Iterator) {
526 18
                $this->sourceResult = $result;
527 18
            } elseif ($result instanceof \Traversable || is_array($result)) {
528 18
                $iterator = new \ArrayIterator();
529 18
                foreach ($result as $file) {
530 13
                    $iterator->append(
531
                        $file instanceof FileInfo
532 2
                            ? $file
533 13
                            : new FileInfo($file, getcwd(), $file)
534
                    );
535
                }
536 18
                $this->sourceResult = $iterator;
537
            } else {
538
                throw new \RuntimeException(
539
                    sprintf(
540
                        'Iterator or array was expected instead of %s.',
541
                        is_object($result) ? get_class($result) : gettype($result)
542
                    )
543
                );
544
            }
545
        }
546
547 36
        return $this->sourceResult;
548
    }
549
550
    /**
551
     * @return int
552
     */
553 3
    public function count()
554
    {
555 3
        return count($this->toArray());
556
    }
557
558
    //endregion
559
}
560