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

FileList::sqlHasStatements()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

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