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

FileList::sqlKeywordsToClasses()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 7
ccs 0
cts 4
cp 0
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
crap 2
1
<?php
2
3
namespace uuf6429\ElderBrother\Change;
4
5
use SqlParser\Statement;
6
use Symfony\Component\Finder\Comparator as SfyComparator;
7
use Symfony\Component\Finder\Iterator as SfyIterator;
8
use SqlParser\Parser as SqlParser;
9
use SqlParser\Statements;
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
     * @return FileList
335
     */
336 5
    public function sqlStatementFilter(\Closure $closure, $recursive = false, $subKey = null)
337
    {
338 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...
339 1
            '%s(%s,%s)',
340 1
            __FUNCTION__,
341
            spl_object_hash($closure),
342 5
            var_export($recursive, true)
343
        );
344
345 5
        return new self(
346 5
            $this->cacheKey . '->' . $subKey,
347
            function () use ($closure, $recursive) {
348 4
                return new Iterator\SqlStatementFilterIterator(
349 4
                    $this->getSourceIterator(),
350
                    $closure,
351
                    $recursive
352
                );
353 5
            }
354
        );
355
    }
356
357
    /**
358
     * Helper function to check if sql code has any of the specified statements.
359
     *
360
     * @param string[] $statementClasses The statement classes to look for
361
     * @param bool     $filterIn         True to "filter in", false to "filter out"
362
     *
363
     * @return FileList
364
     */
365 4
    protected function sqlHasStatements($statementClasses, $filterIn)
366
    {
367 4
        return $this->sqlStatementFilter(
368
            function (Statement $statement) use ($statementClasses, $filterIn) {
369 3
                foreach ($statementClasses as $class) {
370 3
                    if ($statement instanceof $class) {
371 3
                        return $filterIn;
372
                    }
373
                }
374
375 3
                return !$filterIn;
376 4
            },
377 4
            true,
378 4
            __FUNCTION__ . '(' . implode(',', $statementClasses) . ')'
379
        );
380
    }
381
382
    /**
383
     * @param string[] $keywords
384
     * @return string[]
385
     */
386
    protected function sqlKeywordsToClasses($keywords)
387
    {
388
        $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...
389
        $statements = array_replace(array_flip($keywords), SqlParser::$STATEMENT_PARSERS);
390
391
        return array_filter(array_unique($statements));
392
    }
393
394
    /**
395
     * @param string[]|null $keywords
396
     *
397
     * @return FileList
398
     */
399 2
    public function sqlWithDDL($keywords = null)
400
    {
401 2
        $classes = is_null($keywords)
402 2
            ? static::$statementTypes['ddl']
403 2
            : $this->sqlKeywordsToClasses($keywords);
404
405 2
        return $this->sqlHasStatements($classes, true);
406
    }
407
408
    /**
409
     * @param string[]|null $keywords
410
     *
411
     * @return FileList
412
     */
413 1
    public function sqlWithoutDDL($keywords = null)
414
    {
415 1
        $classes = is_null($keywords)
416 1
            ? static::$statementTypes['ddl']
417 1
            : $this->sqlKeywordsToClasses($keywords);
418
419 1
        return $this->sqlHasStatements($classes, false);
420
    }
421
422
    /**
423
     * @param string[]|null $keywords
424
     *
425
     * @return FileList
426
     */
427 1
    public function sqlWithDML($keywords = null)
428
    {
429 1
        $classes = is_null($keywords)
430 1
            ? static::$statementTypes['dml']
431 1
            : $this->sqlKeywordsToClasses($keywords);
432
433 1
        return $this->sqlHasStatements($classes, true);
434
    }
435
436
    /**
437
     * @param string[]|null $keywords
438
     *
439
     * @return FileList
440
     */
441 1
    public function sqlWithoutDML($keywords = null)
442
    {
443 1
        $classes = is_null($keywords)
444 1
            ? static::$statementTypes['dml']
445 1
            : $this->sqlKeywordsToClasses($keywords);
446
447 1
        return $this->sqlHasStatements($classes, false);
448
    }
449
450
    /**
451
     * @param string[]|null $keywords
452
     *
453
     * @return FileList
454
     */
455 1
    public function sqlWithTCL($keywords = null)
456
    {
457 1
        $classes = is_null($keywords)
458 1
            ? static::$statementTypes['tcl']
459 1
            : $this->sqlKeywordsToClasses($keywords);
460
461 1
        return $this->sqlHasStatements($classes, true);
462
    }
463
464
    /**
465
     * @param string[]|null $keywords
466
     *
467
     * @return FileList
468
     */
469
    public function sqlWithoutTCL($keywords = null)
470
    {
471
        $classes = is_null($keywords)
472
            ? static::$statementTypes['tcl']
473
            : $this->sqlKeywordsToClasses($keywords);
474
475
        return $this->sqlHasStatements($classes, false);
476
    }
477
478
    // 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...
479
    // 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...
480
    // 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...
481
482
    //endregion
483
484
    //region Iterator / Interface implementations
485
486
    /**
487
     * Returns array of file paths.
488
     *
489
     * @return string[]
490
     */
491 37
    public function toArray()
492
    {
493 37
        if (!isset(self::$cache[$this->cacheKey])) {
494 36
            self::$cache[$this->cacheKey] = array_map(
495 36
                function (FileInfo $file) {
496 31
                    return $file->getPathname();
497 36
                },
498 36
                array_values(iterator_to_array($this->getSourceIterator()))
499
            );
500
        }
501
502 37
        return self::$cache[$this->cacheKey];
503
    }
504
505
    /**
506
     * @return \Iterator
507
     */
508
    public function getIterator()
509
    {
510
        return new \ArrayIterator($this->toArray());
511
    }
512
513
    /**
514
     * @return \Iterator
515
     */
516 36
    public function getSourceIterator()
517
    {
518 36
        if (!$this->sourceResult) {
519 36
            $source = $this->source;
520 36
            $result = $source();
521
522 36
            if ($result instanceof \IteratorAggregate) {
523
                $this->sourceResult = $result->getIterator();
524
            } elseif ($result instanceof \Iterator) {
525 18
                $this->sourceResult = $result;
526 18
            } elseif ($result instanceof \Traversable || is_array($result)) {
527 18
                $iterator = new \ArrayIterator();
528 18
                foreach ($result as $file) {
529 13
                    $iterator->append(
530
                        $file instanceof FileInfo
531 2
                            ? $file
532 13
                            : new FileInfo($file, getcwd(), $file)
533
                    );
534
                }
535 18
                $this->sourceResult = $iterator;
536
            } else {
537
                throw new \RuntimeException(
538
                    sprintf(
539
                        'Iterator or array was expected instead of %s.',
540
                        is_object($result) ? get_class($result) : gettype($result)
541
                    )
542
                );
543
            }
544
        }
545
546 36
        return $this->sourceResult;
547
    }
548
549
    /**
550
     * @return int
551
     */
552 3
    public function count()
553
    {
554 3
        return count($this->toArray());
555
    }
556
557
    //endregion
558
}
559