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