1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Iterator; |
4: | |
5: | use Salient\Contract\Core\Immutable; |
6: | use Salient\Contract\Iterator\FluentIteratorInterface; |
7: | use Salient\Iterator\Concern\FluentIteratorTrait; |
8: | use Salient\Utility\Exception\FilesystemErrorException; |
9: | use Salient\Utility\Regex; |
10: | use AppendIterator; |
11: | use CallbackFilterIterator; |
12: | use Countable; |
13: | use EmptyIterator; |
14: | use FilesystemIterator; |
15: | use IteratorAggregate; |
16: | use LogicException; |
17: | use RecursiveCallbackFilterIterator; |
18: | use RecursiveDirectoryIterator; |
19: | use RecursiveIteratorIterator; |
20: | use SplFileInfo; |
21: | use Traversable; |
22: | |
23: | |
24: | |
25: | |
26: | |
27: | |
28: | |
29: | class RecursiveFilesystemIterator implements |
30: | IteratorAggregate, |
31: | FluentIteratorInterface, |
32: | Immutable, |
33: | Countable |
34: | { |
35: | |
36: | use FluentIteratorTrait; |
37: | |
38: | private bool $GetFiles = true; |
39: | private bool $GetDirs = false; |
40: | private bool $DirsFirst = true; |
41: | private bool $Recurse = true; |
42: | private bool $MatchRelative = false; |
43: | |
44: | private array $Dirs = []; |
45: | |
46: | private array $Exclude = []; |
47: | |
48: | private array $Include = []; |
49: | |
50: | |
51: | |
52: | |
53: | |
54: | |
55: | public function in(string ...$dirs) |
56: | { |
57: | if (!$dirs) { |
58: | return $this; |
59: | } |
60: | |
61: | $clone = clone $this; |
62: | array_push($clone->Dirs, ...$dirs); |
63: | return $clone; |
64: | } |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | |
72: | |
73: | public function files(bool $value = true) |
74: | { |
75: | if ($this->GetFiles === $value) { |
76: | return $this; |
77: | } |
78: | |
79: | $clone = clone $this; |
80: | $clone->GetFiles = $value; |
81: | return $clone; |
82: | } |
83: | |
84: | |
85: | |
86: | |
87: | |
88: | |
89: | |
90: | |
91: | public function noDirs() |
92: | { |
93: | return $this->dirs(false)->files(); |
94: | } |
95: | |
96: | |
97: | |
98: | |
99: | |
100: | |
101: | |
102: | |
103: | public function dirs(bool $value = true) |
104: | { |
105: | if ($this->GetDirs === $value) { |
106: | return $this; |
107: | } |
108: | |
109: | $clone = clone $this; |
110: | $clone->GetDirs = $value; |
111: | return $clone; |
112: | } |
113: | |
114: | |
115: | |
116: | |
117: | |
118: | |
119: | public function noFiles() |
120: | { |
121: | return $this->files(false)->dirs(); |
122: | } |
123: | |
124: | |
125: | |
126: | |
127: | |
128: | |
129: | |
130: | |
131: | |
132: | |
133: | public function dirsFirst(bool $value = true) |
134: | { |
135: | if ($this->DirsFirst === $value) { |
136: | return $this; |
137: | } |
138: | |
139: | $clone = clone $this; |
140: | $clone->DirsFirst = $value; |
141: | return $clone; |
142: | } |
143: | |
144: | |
145: | |
146: | |
147: | |
148: | |
149: | |
150: | |
151: | public function dirsLast() |
152: | { |
153: | return $this->dirsFirst(false); |
154: | } |
155: | |
156: | |
157: | |
158: | |
159: | |
160: | |
161: | |
162: | |
163: | public function recurse(bool $value = true) |
164: | { |
165: | if ($this->Recurse === $value) { |
166: | return $this; |
167: | } |
168: | |
169: | $clone = clone $this; |
170: | $clone->Recurse = $value; |
171: | return $clone; |
172: | } |
173: | |
174: | |
175: | |
176: | |
177: | |
178: | |
179: | public function doNotRecurse() |
180: | { |
181: | return $this->recurse(false); |
182: | } |
183: | |
184: | |
185: | |
186: | |
187: | |
188: | |
189: | |
190: | public function exclude($value) |
191: | { |
192: | $this->Exclude[] = $value; |
193: | return $this; |
194: | } |
195: | |
196: | |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | public function include($value) |
206: | { |
207: | $this->Include[] = $value; |
208: | return $this; |
209: | } |
210: | |
211: | |
212: | |
213: | |
214: | |
215: | |
216: | |
217: | |
218: | |
219: | |
220: | |
221: | public function matchRelative(bool $value = true) |
222: | { |
223: | if ($this->MatchRelative === $value) { |
224: | return $this; |
225: | } |
226: | |
227: | $clone = clone $this; |
228: | $clone->MatchRelative = $value; |
229: | return $clone; |
230: | } |
231: | |
232: | |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | public function doNotMatchRelative() |
240: | { |
241: | return $this->matchRelative(false); |
242: | } |
243: | |
244: | |
245: | |
246: | |
247: | public function count(): int |
248: | { |
249: | return iterator_count($this->getIterator()); |
250: | } |
251: | |
252: | |
253: | |
254: | |
255: | public function getIterator(): Traversable |
256: | { |
257: | if (!$this->Dirs || (!$this->GetFiles && !$this->GetDirs)) { |
258: | return new EmptyIterator(); |
259: | } |
260: | |
261: | $flags = |
262: | FilesystemIterator::KEY_AS_PATHNAME |
263: | | FilesystemIterator::CURRENT_AS_FILEINFO |
264: | | FilesystemIterator::SKIP_DOTS |
265: | | FilesystemIterator::UNIX_PATHS; |
266: | |
267: | $excludeFilter = |
268: | $this->Exclude |
269: | ? function ( |
270: | SplFileInfo $file, |
271: | string $path, |
272: | FilesystemIterator $iterator |
273: | ): bool { |
274: | $path = $this->getPath($path, $iterator); |
275: | |
276: | foreach ($this->Exclude as $exclude) { |
277: | if (is_callable($exclude)) { |
278: | if ($exclude($file, $path, $iterator)) { |
279: | return true; |
280: | } |
281: | continue; |
282: | } |
283: | if (Regex::match($exclude, $path) |
284: | || ($this->MatchRelative |
285: | && Regex::match($exclude, "/{$path}"))) { |
286: | return true; |
287: | } |
288: | if ($file->isDir() |
289: | && (Regex::match($exclude, "{$path}/") |
290: | || ($this->MatchRelative |
291: | && Regex::match($exclude, "/{$path}/")))) { |
292: | return true; |
293: | } |
294: | } |
295: | |
296: | return false; |
297: | } |
298: | : null; |
299: | |
300: | $includeFilter = |
301: | $this->Include |
302: | ? function ( |
303: | SplFileInfo $file, |
304: | string $path, |
305: | FilesystemIterator $iterator, |
306: | ?RecursiveIteratorIterator $recursiveIterator = null |
307: | ): bool { |
308: | $path = $this->getPath($path, $iterator); |
309: | |
310: | foreach ($this->Include as $include) { |
311: | if (is_callable($include)) { |
312: | if ($include($file, $path, $iterator, $recursiveIterator)) { |
313: | return true; |
314: | } |
315: | continue; |
316: | } |
317: | if (Regex::match($include, $path) |
318: | || ($this->MatchRelative |
319: | && Regex::match($include, "/{$path}"))) { |
320: | return true; |
321: | } |
322: | if ($file->isDir() |
323: | && (Regex::match($include, "$path/") |
324: | || ($this->MatchRelative |
325: | && Regex::match($include, "/{$path}/")))) { |
326: | return true; |
327: | } |
328: | } |
329: | |
330: | return false; |
331: | } |
332: | : null; |
333: | |
334: | foreach ($this->Dirs as $directory) { |
335: | if (!is_dir($directory)) { |
336: | throw new FilesystemErrorException(sprintf('Not a directory: %s', $directory)); |
337: | } |
338: | |
339: | if (!$this->Recurse) { |
340: | $iterator = new FilesystemIterator($directory, $flags); |
341: | |
342: | if ($excludeFilter || $includeFilter) { |
343: | $iterator = new CallbackFilterIterator( |
344: | $iterator, |
345: | fn(SplFileInfo $file, string $path, FilesystemIterator $iterator): bool => |
346: | (!$excludeFilter || !$excludeFilter($file, $path, $iterator)) |
347: | && (!$includeFilter || $includeFilter($file, $path, $iterator)) |
348: | ); |
349: | } |
350: | |
351: | if (!$this->GetDirs) { |
352: | $iterator = new CallbackFilterIterator( |
353: | $iterator, |
354: | fn(SplFileInfo $file): bool => !$file->isDir(), |
355: | ); |
356: | } |
357: | |
358: | if (!$this->GetFiles) { |
359: | $iterator = new CallbackFilterIterator( |
360: | $iterator, |
361: | fn(SplFileInfo $file): bool => !$file->isFile(), |
362: | ); |
363: | } |
364: | |
365: | $iterators[] = $iterator; |
366: | |
367: | continue; |
368: | } |
369: | |
370: | $mode = |
371: | $this->GetDirs |
372: | ? ($this->DirsFirst |
373: | ? RecursiveIteratorIterator::SELF_FIRST |
374: | : RecursiveIteratorIterator::CHILD_FIRST) |
375: | : RecursiveIteratorIterator::LEAVES_ONLY; |
376: | |
377: | $iterator = new RecursiveDirectoryIterator($directory, $flags); |
378: | |
379: | |
380: | |
381: | if ($excludeFilter) { |
382: | $iterator = new RecursiveCallbackFilterIterator( |
383: | $iterator, |
384: | fn(SplFileInfo $file, string $path, RecursiveDirectoryIterator $iterator): bool => |
385: | !$excludeFilter($file, $path, $iterator) |
386: | ); |
387: | } |
388: | |
389: | $iterator = new RecursiveIteratorIterator($iterator, $mode); |
390: | |
391: | |
392: | |
393: | if ($includeFilter) { |
394: | $iterator = new CallbackFilterIterator( |
395: | $iterator, |
396: | function (SplFileInfo $file, string $path, RecursiveIteratorIterator $iterator) use ($includeFilter): bool { |
397: | $recursiveIterator = $iterator; |
398: | |
399: | $iterator = $iterator->getInnerIterator(); |
400: | if ($iterator instanceof RecursiveCallbackFilterIterator) { |
401: | |
402: | $iterator = $iterator->getInnerIterator(); |
403: | } |
404: | return $includeFilter($file, $path, $iterator, $recursiveIterator); |
405: | } |
406: | ); |
407: | } |
408: | |
409: | if (!$this->GetFiles) { |
410: | $iterator = new CallbackFilterIterator( |
411: | $iterator, |
412: | fn(SplFileInfo $file): bool => !$file->isFile(), |
413: | ); |
414: | } |
415: | |
416: | $iterators[] = $iterator; |
417: | } |
418: | |
419: | if (count($iterators) === 1) { |
420: | return $iterators[0]; |
421: | } |
422: | |
423: | $iterator = new AppendIterator(); |
424: | foreach ($iterators as $dirIterator) { |
425: | $iterator->append($dirIterator); |
426: | } |
427: | |
428: | return $iterator; |
429: | } |
430: | |
431: | |
432: | |
433: | |
434: | public function nextWithValue($key, $value, bool $strict = false) |
435: | { |
436: | $name = Regex::replace('/^(?:get|is)/i', '', (string) $key, -1, $count); |
437: | |
438: | if (method_exists(SplFileInfo::class, $key)) { |
439: | |
440: | |
441: | if (!$count) { |
442: | throw new LogicException(sprintf('Illegal key: %s', $key)); |
443: | } |
444: | $method = $key; |
445: | } else { |
446: | foreach (["get{$name}", "is{$name}"] as $method) { |
447: | if (method_exists(SplFileInfo::class, $method)) { |
448: | break; |
449: | } |
450: | $method = null; |
451: | } |
452: | } |
453: | |
454: | if ( |
455: | $method === null |
456: | || !strcasecmp($method, 'getFileInfo') |
457: | || !strcasecmp($method, 'getPathInfo') |
458: | ) { |
459: | throw new LogicException(sprintf('Invalid key: %s', $key)); |
460: | } |
461: | |
462: | foreach ($this as $current) { |
463: | $_value = $current->$method(); |
464: | if ($strict) { |
465: | if ($_value === $value) { |
466: | return $current; |
467: | } |
468: | } elseif ($_value == $value) { |
469: | return $current; |
470: | } |
471: | } |
472: | |
473: | return null; |
474: | } |
475: | |
476: | private function getPath(string $path, FilesystemIterator $iterator): string |
477: | { |
478: | if (!$this->MatchRelative) { |
479: | return $path; |
480: | } |
481: | |
482: | if ($iterator instanceof RecursiveDirectoryIterator) { |
483: | return $iterator->getSubPathname(); |
484: | } |
485: | |
486: | return basename($path); |
487: | } |
488: | } |
489: | |