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

FileList::sqlHasStatements()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 17
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 17
ccs 9
cts 9
cp 1
rs 9.2
cc 4
eloc 9
nc 1
nop 2
crap 4
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 36
    public function __construct($cacheKey, callable $source)
29
    {
30 36
        $this->cacheKey = $cacheKey;
31 36
        $this->source = $source;
32 36
    }
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 7
    protected function filterByClosure($subKey, \Closure $closure)
293
    {
294 7
        return new self(
295 7
            $this->cacheKey . '->' . $subKey,
296
            function () use ($closure) {
297 7
                return new Iterator\CustomFilterIterator(
298 7
                    $this->getSourceIterator(),
299 7
                    [$closure]
300
                );
301 7
            }
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 2
    protected function sqlHasStatements($statementClasses, $filterIn)
349
    {
350 2
        return $this->filterByClosure(
351 2
            __FUNCTION__ . '(' . implode(',', $statementClasses) . ')',
352
            function (FileInfo $file) use ($statementClasses, $filterIn) {
353 2
                foreach($file->getSqlParser()->statements as $statement){
354 2
                    foreach ($statementClasses as $statementClass) {
355 2
                        if ($statement instanceof $statementClass) {
356 2
                            return $filterIn;
357
                        }
358
                    }
359
                }
360
361 2
                return !$filterIn;
362 2
            }
363
        );
364
    }
365
366
    /**
367
     * @param string[] $keywords
368
     * @return string[]
369
     */
370
    protected function sqlKeywordsToClasses($keywords)
371
    {
372
        $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...
373
        $statements = array_replace(array_flip($keywords), SqlParser::$STATEMENT_PARSERS);
374
375
        return array_filter(array_unique($statements));
376
    }
377
378
    /**
379
     * @param string[]|null $keywords
380
     *
381
     * @return FileList
382
     */
383 2
    public function sqlWithDDL($keywords = null)
384
    {
385 2
        $classes = is_null($keywords)
386 2
            ? static::$statementTypes['ddl']
387 2
            : $this->sqlKeywordsToClasses($keywords);
388
389 2
        return $this->sqlHasStatements($classes, true);
390
    }
391
392
    /**
393
     * @param string[]|null $keywords
394
     *
395
     * @return FileList
396
     */
397
    public function sqlWithoutDDL($keywords = null)
398
    {
399
        $classes = is_null($keywords)
400
            ? static::$statementTypes['ddl']
401
            : $this->sqlKeywordsToClasses($keywords);
402
403
        return $this->sqlHasStatements($classes, false);
404
    }
405
406
    /**
407
     * @param string[]|null $keywords
408
     *
409
     * @return FileList
410
     */
411 1
    public function sqlWithDML($keywords = null)
412
    {
413 1
        $classes = is_null($keywords)
414 1
            ? static::$statementTypes['dml']
415 1
            : $this->sqlKeywordsToClasses($keywords);
416
417 1
        return $this->sqlHasStatements($classes, true);
418
    }
419
420
    /**
421
     * @param string[]|null $keywords
422
     *
423
     * @return FileList
424
     */
425
    public function sqlWithoutDML($keywords = null)
426
    {
427
        $classes = is_null($keywords)
428
            ? static::$statementTypes['dml']
429
            : $this->sqlKeywordsToClasses($keywords);
430
431
        return $this->sqlHasStatements($classes, false);
432
    }
433
434
    /**
435
     * @param string[]|null $keywords
436
     *
437
     * @return FileList
438
     */
439
    public function sqlWithTCL($keywords = null)
440
    {
441
        $classes = is_null($keywords)
442
            ? static::$statementTypes['tcl']
443
            : $this->sqlKeywordsToClasses($keywords);
444
445
        return $this->sqlHasStatements($classes, true);
446
    }
447
448
    /**
449
     * @param string[]|null $keywords
450
     *
451
     * @return FileList
452
     */
453
    public function sqlWithoutTCL($keywords = null)
454
    {
455
        $classes = is_null($keywords)
456
            ? static::$statementTypes['tcl']
457
            : $this->sqlKeywordsToClasses($keywords);
458
459
        return $this->sqlHasStatements($classes, false);
460
    }
461
462
    // 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...
463
    // 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...
464
    // 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...
465
466
    //endregion
467
468
    //region Iterator / Interface implementations
469
470
    /**
471
     * Returns array of file paths.
472
     *
473
     * @return string[]
474
     */
475 35
    public function toArray()
476
    {
477 35
        if (!isset(self::$cache[$this->cacheKey])) {
478 35
            self::$cache[$this->cacheKey] = array_map(
479 35
                function (FileInfo $file) {
480 30
                    return $file->getPathname();
481 35
                },
482 35
                array_values(iterator_to_array($this->getSourceIterator()))
483
            );
484
        }
485
486 35
        return self::$cache[$this->cacheKey];
487
    }
488
489
    /**
490
     * @return \Iterator
491
     */
492
    public function getIterator()
493
    {
494
        return new \ArrayIterator($this->toArray());
495
    }
496
497
    /**
498
     * @return \Iterator
499
     */
500 35
    public function getSourceIterator()
501
    {
502 35
        if (!$this->sourceResult) {
503 35
            $source = $this->source;
504 35
            $result = $source();
505
506 35
            if ($result instanceof \IteratorAggregate) {
507
                $this->sourceResult = $result->getIterator();
508
            } elseif ($result instanceof \Iterator) {
509 17
                $this->sourceResult = $result;
510 18
            } elseif ($result instanceof \Traversable || is_array($result)) {
511 18
                $iterator = new \ArrayIterator();
512 18
                foreach ($result as $file) {
513 13
                    $iterator->append(
514
                        $file instanceof FileInfo
515 2
                            ? $file
516 13
                            : new FileInfo($file, getcwd(), $file)
517
                    );
518
                }
519 18
                $this->sourceResult = $iterator;
520
            } else {
521
                throw new \RuntimeException(
522
                    sprintf(
523
                        'Iterator or array was expected instead of %s.',
524
                        is_object($result) ? get_class($result) : gettype($result)
525
                    )
526
                );
527
            }
528
        }
529
530 35
        return $this->sourceResult;
531
    }
532
533
    /**
534
     * @return int
535
     */
536 3
    public function count()
537
    {
538 3
        return count($this->toArray());
539
    }
540
541
    //endregion
542
}
543