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

FileList::sqlWithoutDDL()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 6
ccs 0
cts 3
cp 0
rs 9.4285
cc 2
eloc 3
nc 2
nop 1
crap 6
1
<?php
2
3
namespace uuf6429\ElderBrother\Change;
4
5
use PHPSQLParser\utils\PHPSQLParserConstants;
6
use Symfony\Component\Finder\Comparator;
7
use Symfony\Component\Finder\Iterator;
8
9
class FileList implements \IteratorAggregate, \Countable
10
{
11
    /** @var string */
12
    protected $cacheKey;
13
14
    /** @var callable */
15
    protected $source;
16
17
    /** @var array */
18
    protected static $cache;
19
20
    /** @var \Iterator */
21
    protected $sourceResult;
22
23
    /**
24
     * @param string   $cacheKey Unique key to identify this collection of files
25
     * @param callable $source   Callable that return an iterator or array of FileInfo
26
     */
27 34
    public function __construct($cacheKey, callable $source)
28
    {
29 34
        $this->cacheKey = $cacheKey;
30 34
        $this->source = $source;
31 34
    }
32
33
    #region File / Path Name filtering
34
35
    /**
36
     * Search file names (excluding path) by pattern.
37
     *
38
     * @param string $pattern Pattern to look for in file name (regexp, glob, or string)
39
     *
40
     * @return static
41
     */
42 5
    public function name($pattern)
43
    {
44 5
        return new self(
45 5
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
46
            function () use ($pattern) {
47 5
                return new Iterator\FilenameFilterIterator(
48 5
                    $this->getSourceIterator(),
49 5
                    [$pattern],
50 5
                    []
51
                );
52 5
            }
53
        );
54
    }
55
56
    /**
57
     * Search file names (excluding path) by pattern.
58
     *
59
     * @param string $pattern Pattern to exclude files (regexp, glob, or string)
60
     *
61
     * @return static
62
     */
63 1
    public function notName($pattern)
64
    {
65 1
        return new self(
66 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
67
            function () use ($pattern) {
68 1
                return new Iterator\FilenameFilterIterator(
69 1
                    $this->getSourceIterator(),
70 1
                    [],
71 1
                    [$pattern]
72
                );
73 1
            }
74
        );
75
    }
76
77
    /**
78
     * Search path names by pattern.
79
     *
80
     * @param string $pattern Pattern to look for in path (regexp, glob, or string)
81
     *
82
     * @return static
83
     */
84 1
    public function path($pattern)
85
    {
86 1
        return new self(
87 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
88
            function () use ($pattern) {
89 1
                return new Iterator\PathFilterIterator(
90 1
                    $this->getSourceIterator(),
91 1
                    [$pattern],
92 1
                    []
93
                );
94 1
            }
95
        );
96
    }
97
98
    /**
99
     * Search path names by pattern.
100
     *
101
     * @param string $pattern Pattern to exclude paths (regexp, glob, or string)
102
     *
103
     * @return static
104
     */
105 1
    public function notPath($pattern)
106
    {
107 1
        return new self(
108 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
109
            function () use ($pattern) {
110 1
                return new Iterator\PathFilterIterator(
111 1
                    $this->getSourceIterator(),
112 1
                    [],
113 1
                    [$pattern]
114
                );
115 1
            }
116
        );
117
    }
118
119
    #endregion
120
121
    #region FS Item Type Filtering
122
123
    /**
124
     * Filters out anything that is not a file.
125
     *
126
     * @return static
127
     */
128 3
    public function files()
129
    {
130 3
        return new self(
131 3
            $this->cacheKey . '->' . __FUNCTION__ . '()',
132
            function () {
133 3
                return new Iterator\FileTypeFilterIterator(
134 3
                    $this->getSourceIterator(),
135 3
                    Iterator\FileTypeFilterIterator::ONLY_FILES
136
                );
137 3
            }
138
        );
139
    }
140
141
    /**
142
     * Filters out anything that is not a directory.
143
     *
144
     * @return static
145
     */
146 2
    public function directories()
147
    {
148 2
        return new self(
149 2
            $this->cacheKey . '->' . __FUNCTION__ . '()',
150
            function () {
151 2
                return new Iterator\FileTypeFilterIterator(
152 2
                    $this->getSourceIterator(),
153 2
                    Iterator\FileTypeFilterIterator::ONLY_DIRECTORIES
154
                );
155 2
            }
156
        );
157
    }
158
159
    #endregion
160
161
    /**
162
     * Filters out items that do not match the specified level.
163
     *
164
     * @param string $level The depth expression (for example '< 1')
165
     *
166
     * @return static
167
     */
168 3
    public function depth($level)
169
    {
170 3
        $minDepth = 0;
171 3
        $maxDepth = PHP_INT_MAX;
172 3
        $comparator = new Comparator\NumberComparator($level);
173 3
        $comparatorTarget = intval($comparator->getTarget());
174
175 3
        switch ($comparator->getOperator()) {
176 3
            case '>':
177 1
                $minDepth = $comparatorTarget + 1;
178 1
                break;
179
180 2
            case '>=':
181
                $minDepth = $comparatorTarget;
182
                break;
183
184 2
            case '<':
185 2
                $maxDepth = $comparatorTarget - 1;
186 2
                break;
187
188
            case '<=':
189
                $maxDepth = $comparatorTarget;
190
                break;
191
192
            default:
193
                $minDepth = $maxDepth = $comparatorTarget;
194
                break;
195
        }
196
197 3
        return $this->filter(
198
            function (FileInfo $file) use ($minDepth, $maxDepth) {
199 3
                $depth = count(explode('/', str_replace('\\', '/', $file->getRelativePathname()))) - 1;
200
201 3
                return $depth >= $minDepth && $depth <= $maxDepth;
202 3
            }
203
        );
204
    }
205
206
    /**
207
     * Filters out items whose last modified do not match expression.
208
     *
209
     * @param string $date A date range string that can be parsed by `strtotime()``
210
     *
211
     * @return static
212
     */
213
    public function date($date)
214
    {
215
        return new self(
216
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $date . ')',
217
            function () use ($date) {
218
                return new Iterator\DateRangeFilterIterator(
219
                    $this->getSourceIterator(),
220
                    [new Comparator\DateComparator($date)]
221
                );
222
            }
223
        );
224
    }
225
226
    /**
227
     * Filters out files not matching a string or regexp.
228
     *
229
     * @param string $pattern A pattern (string or regexp)
230
     *
231
     * @return static
232
     */
233 2
    public function contains($pattern)
234
    {
235 2
        return new self(
236 2
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
237
            function () use ($pattern) {
238 2
                return new Iterator\FilecontentFilterIterator(
239 2
                    $this->getSourceIterator(),
240 2
                    [$pattern],
241 2
                    []
242
                );
243 2
            }
244
        );
245
    }
246
247
    /**
248
     * Filters out files matching a string or regexp.
249
     *
250
     * @param string $pattern A pattern (string or regexp)
251
     *
252
     * @return static
253
     */
254 1
    public function notContains($pattern)
255
    {
256 1
        return new self(
257 1
            $this->cacheKey . '->' . __FUNCTION__ . '(' . $pattern . ')',
258
            function () use ($pattern) {
259 1
                return new Iterator\FilecontentFilterIterator(
260 1
                    $this->getSourceIterator(),
261 1
                    [],
262 1
                    [$pattern]
263
                );
264 1
            }
265
        );
266
    }
267
268
    /**
269
     * Filters using an anonymous function. Function receives a Change\FileInfo and must return false to filter it out.
270
     *
271
     * @param \Closure $closure An anonymous function
272
     *
273
     * @return static
274
     */
275 4
    public function filter(\Closure $closure)
276
    {
277 4
        return $this->filterByClosure(
278 4
            __FUNCTION__ . '(' . spl_object_hash($closure) . ')',
279
            $closure
280
        );
281
    }
282
283
    /**
284
     * Helper method that can be reused in more specific methods.
285
     *
286
     * @param string $subKey The cache sub key to use.
287
     * @param \Closure $closure The filtering callback.
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 boolean $filterIn True to "filter in", false to "filter out".
346
     * @return FileList
347
     */
348 1
    protected function sqlKeywords($keywords, $filterIn)
349
    {
350 1
        return $this->filterByClosure(
351 1
            __FUNCTION__ . '(' . is_null($keywords) ? '*' : implode(',', $keywords) . ')',
352
            function (FileInfo $file) use ($keywords, $filterIn) {
353 1
                if (!($sql = $file->getParsedSql())) {
354 1
                    return !$filterIn;
355
                }
356
357
                // 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...
358 1
                foreach ($keywords as $keyword) {
359 1
                    if (array_key_exists($keyword, $sql)) {
360 1
                        return $filterIn;
361
                    }
362
                }
363
364 1
                return !$filterIn;
365 1
            }
366
        );
367
    }
368
369
    /**
370
     * @param string[]|null $keywords
371
     * @return FileList
372
     */
373
    public function sqlWithDCL($keywords = null)
374
    {
375
        $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...
376
377
        return $this->sqlKeywords($keywords, true);
378
    }
379
380
    /**
381
     * @param string[]|null $keywords
382
     * @return FileList
383
     */
384
    public function sqlWithoutDCL($keywords = null)
385
    {
386
        $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...
387
388
        return $this->sqlKeywords($keywords, false);
389
    }
390
391
    /**
392
     * @param string[]|null $keywords
393
     * @return FileList
394
     */
395 1
    public function sqlWithDDL($keywords = null)
396
    {
397 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...
398
399 1
        return $this->sqlKeywords($keywords, true);
400
    }
401
402
    /**
403
     * @param string[]|null $keywords
404
     * @return FileList
405
     */
406
    public function sqlWithoutDDL($keywords = null)
407
    {
408
        $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...
409
410
        return $this->sqlKeywords($keywords, false);
411
    }
412
413
    /**
414
     * @param string[]|null $keywords
415
     * @return FileList
416
     */
417
    public function sqlWithDML($keywords = null)
418
    {
419
        $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...
420
421
        return $this->sqlKeywords($keywords, true);
422
    }
423
424
    /**
425
     * @param string[]|null $keywords
426
     * @return FileList
427
     */
428
    public function sqlWithoutDML($keywords = null)
429
    {
430
        $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...
431
432
        return $this->sqlKeywords($keywords, false);
433
    }
434
435
    /**
436
     * @param string[]|null $keywords
437
     * @return FileList
438
     */
439
    public function sqlWithDQL($keywords = null)
440
    {
441
        $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...
442
443
        return $this->sqlKeywords($keywords, true);
444
    }
445
446
    /**
447
     * @param string[]|null $keywords
448
     * @return FileList
449
     */
450
    public function sqlWithoutDQL($keywords = null)
451
    {
452
        $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...
453
454
        return $this->sqlKeywords($keywords, false);
455
    }
456
457
    /**
458
     * @param string[]|null $keywords
459
     * @return FileList
460
     */
461
    public function sqlWithTCL($keywords = null)
462
    {
463
        $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...
464
465
        return $this->sqlKeywords($keywords, true);
466
    }
467
468
    /**
469
     * @param string[]|null $keywords
470
     * @return FileList
471
     */
472
    public function sqlWithoutTCL($keywords = null)
473
    {
474
        $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...
475
476
        return $this->sqlKeywords($keywords, 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 33
    public function toArray()
493
    {
494 33
        if (!isset(self::$cache[$this->cacheKey])) {
495 33
            self::$cache[$this->cacheKey] = array_map(
496 33
                function (FileInfo $file) {
497 28
                    return $file->getPathname();
498 33
                },
499 33
                array_values(iterator_to_array($this->getSourceIterator()))
500
            );
501
        }
502
503 33
        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 33
    public function getSourceIterator()
518
    {
519 33
        if (!$this->sourceResult) {
520 33
            $source = $this->source;
521 33
            $result = $source();
522
523 33
            if ($result instanceof \IteratorAggregate) {
524
                $this->sourceResult = $result->getIterator();
525
            } elseif ($result instanceof \Iterator) {
526 15
                $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 33
        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