Completed
Pull Request — master (#30)
by Christian
02:50 queued 20s
created

FileList::sqlKeywords()   B

Complexity

Conditions 5
Paths 1

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 5

Importance

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