Completed
Pull Request — master (#30)
by Christian
06:42
created

FileList::sqlWith()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

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