1: | <?php declare(strict_types=1); |
2: | |
3: | namespace Salient\Utility; |
4: | |
5: | use Salient\Core\Process; |
6: | use Salient\Iterator\RecursiveFilesystemIterator; |
7: | use Salient\Utility\Exception\FilesystemErrorException; |
8: | use Salient\Utility\Exception\InvalidRuntimeConfigurationException; |
9: | use Salient\Utility\Exception\UnreadDataException; |
10: | use Salient\Utility\Exception\UnwrittenDataException; |
11: | use Salient\Utility\Internal\IndentationGuesser; |
12: | use Salient\Utility\Support\Indentation; |
13: | use InvalidArgumentException; |
14: | use Stringable; |
15: | |
16: | |
17: | |
18: | |
19: | |
20: | |
21: | |
22: | |
23: | |
24: | final class File extends AbstractUtility |
25: | { |
26: | |
27: | |
28: | |
29: | |
30: | |
31: | |
32: | public static function isAbsolute(string $path): bool |
33: | { |
34: | return (bool) Regex::match( |
35: | '@^(?:/|\\\\\\\\|[a-z]:[/\\\\]|[a-z][-a-z0-9+.]+:)@i', |
36: | $path, |
37: | ); |
38: | } |
39: | |
40: | |
41: | |
42: | |
43: | |
44: | |
45: | |
46: | |
47: | |
48: | public static function resolvePath(string $path, bool $withUriSegments = false): string |
49: | { |
50: | $path = str_replace('\\', '/', $path); |
51: | |
52: | |
53: | $path = Regex::replace('@(?<=/|^)\.(?:/|$)@', '', $path); |
54: | |
55: | |
56: | $regex = $withUriSegments ? '/' : '/+'; |
57: | $regex = "@(?:^|(?<=^/)|(?<=/|^(?!/))(?!\.\.(?:/|\$))[^/]*{$regex})\.\.(?:/|\$)@"; |
58: | do { |
59: | $path = Regex::replace($regex, '', $path, -1, $count); |
60: | } while ($count); |
61: | |
62: | return $path; |
63: | } |
64: | |
65: | |
66: | |
67: | |
68: | |
69: | |
70: | |
71: | public static function sanitiseDir(string $directory): string |
72: | { |
73: | return $directory === '' |
74: | ? '.' |
75: | : Str::coalesce( |
76: | rtrim($directory, \DIRECTORY_SEPARATOR === '/' ? '/' : '\/'), |
77: | \DIRECTORY_SEPARATOR, |
78: | ); |
79: | } |
80: | |
81: | |
82: | |
83: | |
84: | public static function chdir(string $directory): void |
85: | { |
86: | self::check(@chdir($directory), 'chdir', $directory); |
87: | } |
88: | |
89: | |
90: | |
91: | |
92: | public static function mkdir( |
93: | string $directory, |
94: | int $permissions = 0777, |
95: | bool $recursive = false |
96: | ): void { |
97: | self::check(@mkdir($directory, $permissions, $recursive), 'mkdir', $directory); |
98: | } |
99: | |
100: | |
101: | |
102: | |
103: | public static function chmod(string $filename, int $permissions): void |
104: | { |
105: | self::check(@chmod($filename, $permissions), 'chmod', $filename); |
106: | } |
107: | |
108: | |
109: | |
110: | |
111: | public static function rename(string $from, string $to): void |
112: | { |
113: | self::check(@rename($from, $to), 'rename', null, null, $from, $to); |
114: | } |
115: | |
116: | |
117: | |
118: | |
119: | public static function symlink(string $target, string $link): void |
120: | { |
121: | self::check(@symlink($target, $link), 'symlink', null, null, $target, $link); |
122: | } |
123: | |
124: | |
125: | |
126: | |
127: | public static function touch( |
128: | string $filename, |
129: | ?int $mtime = null, |
130: | ?int $atime = null |
131: | ): void { |
132: | $mtime ??= time(); |
133: | $atime ??= $mtime; |
134: | self::check(@touch($filename, $mtime, $atime), 'touch', $filename); |
135: | } |
136: | |
137: | |
138: | |
139: | |
140: | |
141: | |
142: | |
143: | public static function getClosestPath(string $path): ?string |
144: | { |
145: | $pathIsParent = false; |
146: | while (!file_exists($path)) { |
147: | $parent = dirname($path); |
148: | if ($parent === $path) { |
149: | |
150: | return null; |
151: | |
152: | } |
153: | $path = $parent; |
154: | $pathIsParent = true; |
155: | } |
156: | |
157: | if ($pathIsParent && !is_dir($path)) { |
158: | return null; |
159: | } |
160: | |
161: | return $path; |
162: | } |
163: | |
164: | |
165: | |
166: | |
167: | |
168: | |
169: | |
170: | |
171: | |
172: | public static function create( |
173: | string $filename, |
174: | int $permissions = 0777, |
175: | int $dirPermissions = 0777, |
176: | bool $umaskApplies = true |
177: | ): void { |
178: | if (is_file($filename)) { |
179: | return; |
180: | } |
181: | self::createDir(dirname($filename), $dirPermissions, $umaskApplies); |
182: | $umask = umask(); |
183: | if ($umaskApplies) { |
184: | $permissions &= ~$umask; |
185: | } |
186: | try { |
187: | |
188: | |
189: | umask(077); |
190: | $handle = self::open($filename, 'x'); |
191: | self::chmod($filename, $permissions); |
192: | self::close($handle, $filename); |
193: | } finally { |
194: | umask($umask); |
195: | } |
196: | } |
197: | |
198: | |
199: | |
200: | |
201: | |
202: | |
203: | |
204: | |
205: | public static function createDir( |
206: | string $directory, |
207: | int $permissions = 0777, |
208: | bool $umaskApplies = true |
209: | ): void { |
210: | if (is_dir($directory)) { |
211: | return; |
212: | } |
213: | $parent = dirname($directory); |
214: | $umask = umask(); |
215: | if ($umaskApplies) { |
216: | $permissions &= ~$umask; |
217: | } |
218: | try { |
219: | if (!is_dir($parent)) { |
220: | umask(0); |
221: | $parentPerms = $umaskApplies |
222: | ? 0777 & ~$umask |
223: | : 0755; |
224: | self::mkdir($parent, $parentPerms, true); |
225: | } |
226: | umask(077); |
227: | self::mkdir($directory); |
228: | self::chmod($directory, $permissions); |
229: | } finally { |
230: | umask($umask); |
231: | } |
232: | } |
233: | |
234: | |
235: | |
236: | |
237: | |
238: | |
239: | |
240: | |
241: | |
242: | public static function createTemp( |
243: | ?string $directory = null, |
244: | ?string $prefix = null |
245: | ): string { |
246: | if ($directory !== null) { |
247: | $directory = self::sanitiseDir($directory); |
248: | if (!is_dir($directory) || !is_writable($directory)) { |
249: | throw new FilesystemErrorException(sprintf( |
250: | 'Not a writable directory: %s', |
251: | $directory, |
252: | )); |
253: | } |
254: | } |
255: | return self::check(@tempnam( |
256: | $directory ?? Sys::getTempDir(), |
257: | $prefix ?? Sys::getProgramBasename(), |
258: | ), 'tempnam'); |
259: | } |
260: | |
261: | |
262: | |
263: | |
264: | |
265: | |
266: | |
267: | |
268: | |
269: | public static function createTempDir( |
270: | ?string $directory = null, |
271: | ?string $prefix = null |
272: | ): string { |
273: | if ($directory !== null) { |
274: | $directory = self::sanitiseDir($directory); |
275: | } |
276: | $directory ??= Sys::getTempDir(); |
277: | $prefix ??= Sys::getProgramBasename(); |
278: | $failed = false; |
279: | do { |
280: | if ($failed) { |
281: | clearstatcache(); |
282: | if (!is_dir($directory) || !is_writable($directory)) { |
283: | throw new FilesystemErrorException(sprintf( |
284: | 'Not a writable directory: %s', |
285: | $directory, |
286: | )); |
287: | } |
288: | usleep(10000); |
289: | } |
290: | $dir = sprintf( |
291: | '%s/%s%s.tmp', |
292: | $directory, |
293: | $prefix, |
294: | Get::randomText(8), |
295: | ); |
296: | $failed = !@mkdir($dir, 0700); |
297: | } while ($failed); |
298: | self::chmod($dir, 0700); |
299: | return $dir; |
300: | } |
301: | |
302: | |
303: | |
304: | |
305: | public static function delete(string $filename): void |
306: | { |
307: | if (!file_exists($filename)) { |
308: | return; |
309: | } |
310: | if (!is_file($filename)) { |
311: | throw new FilesystemErrorException( |
312: | sprintf('Not a file: %s', $filename), |
313: | ); |
314: | } |
315: | self::check(@unlink($filename), 'unlink', $filename); |
316: | } |
317: | |
318: | |
319: | |
320: | |
321: | public static function deleteDir(string $directory): void |
322: | { |
323: | if (!file_exists($directory)) { |
324: | return; |
325: | } |
326: | if (!is_dir($directory)) { |
327: | throw new FilesystemErrorException( |
328: | sprintf('Not a directory: %s', $directory), |
329: | ); |
330: | } |
331: | self::check(@rmdir($directory), 'rmdir', $directory); |
332: | } |
333: | |
334: | |
335: | |
336: | |
337: | public static function find(): RecursiveFilesystemIterator |
338: | { |
339: | return new RecursiveFilesystemIterator(); |
340: | } |
341: | |
342: | |
343: | |
344: | |
345: | public static function getcwd(): string |
346: | { |
347: | $command = Sys::isWindows() ? 'cd' : 'pwd'; |
348: | if (class_exists(Process::class)) { |
349: | $process = Process::withShellCommand($command); |
350: | if ($process->run() === 0) { |
351: | return $process->getOutputAsText(); |
352: | } |
353: | } else { |
354: | |
355: | $pipe = self::openPipe($command, 'r'); |
356: | $dir = self::getContents($pipe); |
357: | if (self::closePipe($pipe, $command) === 0) { |
358: | return Str::trimNativeEol($dir); |
359: | } |
360: | |
361: | } |
362: | error_clear_last(); |
363: | return self::check(@getcwd(), 'getcwd'); |
364: | } |
365: | |
366: | |
367: | |
368: | |
369: | |
370: | public static function isCreatable(string $path): bool |
371: | { |
372: | $path = self::getClosestPath($path); |
373: | return $path !== null && is_writable($path); |
374: | } |
375: | |
376: | |
377: | |
378: | |
379: | |
380: | |
381: | |
382: | |
383: | public static function pruneDir(string $directory, bool $delete = false, bool $setPermissions = false): void |
384: | { |
385: | $files = (new RecursiveFilesystemIterator()) |
386: | ->in($directory) |
387: | ->dirs(); |
388: | |
389: | if ($setPermissions) { |
390: | clearstatcache(); |
391: | |
392: | |
393: | |
394: | |
395: | |
396: | if (!Sys::isWindows()) { |
397: | foreach ($files->noFiles() as $dir) { |
398: | if ( |
399: | $dir->isReadable() |
400: | && $dir->isWritable() |
401: | && $dir->isExecutable() |
402: | ) { |
403: | continue; |
404: | } |
405: | $perms = @$dir->getPerms(); |
406: | if ($perms === false) { |
407: | |
408: | $perms = 0; |
409: | |
410: | } |
411: | self::chmod((string) $dir, $perms | 0700); |
412: | } |
413: | $setPermissions = false; |
414: | } |
415: | } |
416: | |
417: | foreach ($files->dirsLast() as $file) { |
418: | $filename = (string) $file; |
419: | if ($setPermissions && !$file->isWritable()) { |
420: | |
421: | self::chmod($filename, $file->isDir() ? 0700 : 0600); |
422: | } |
423: | if ($file->isDir()) { |
424: | self::check(@rmdir($filename), 'rmdir', $filename); |
425: | } else { |
426: | self::check(@unlink($filename), 'unlink', $filename); |
427: | } |
428: | } |
429: | |
430: | if ($delete) { |
431: | self::check(@rmdir($directory), 'rmdir', $directory); |
432: | } |
433: | } |
434: | |
435: | |
436: | |
437: | |
438: | |
439: | |
440: | public static function realpath(string $path): string |
441: | { |
442: | if (Str::lower(substr($path, 0, 7)) === 'phar://' && file_exists($path)) { |
443: | return self::resolvePath($path, true); |
444: | } |
445: | error_clear_last(); |
446: | return self::check(@realpath($path), 'realpath', $path); |
447: | } |
448: | |
449: | |
450: | |
451: | |
452: | |
453: | |
454: | |
455: | |
456: | |
457: | |
458: | |
459: | |
460: | |
461: | public static function getRelativePath( |
462: | string $path, |
463: | string $parentDirectory, |
464: | ?string $default = null |
465: | ): ?string { |
466: | $path = self::realpath($path); |
467: | $basePath = self::realpath($parentDirectory); |
468: | if (strpos($path, $basePath) === 0) { |
469: | return substr($path, strlen($basePath) + 1); |
470: | } |
471: | return $default; |
472: | } |
473: | |
474: | |
475: | |
476: | |
477: | public static function same(string $filename1, string $filename2): bool |
478: | { |
479: | if (!file_exists($filename1)) { |
480: | return false; |
481: | } |
482: | if ($filename1 === $filename2) { |
483: | return true; |
484: | } |
485: | if (!file_exists($filename2)) { |
486: | return false; |
487: | } |
488: | $stat1 = self::stat($filename1); |
489: | $stat2 = self::stat($filename2); |
490: | return $stat1['dev'] === $stat2['dev'] |
491: | && $stat1['ino'] === $stat2['ino']; |
492: | } |
493: | |
494: | |
495: | |
496: | |
497: | public static function getIdentifier(string $filename): string |
498: | { |
499: | $stat = self::stat($filename); |
500: | return sprintf('%d:%d', $stat['dev'], $stat['ino']); |
501: | } |
502: | |
503: | |
504: | |
505: | |
506: | public static function size(string $filename): int |
507: | { |
508: | return self::check(@filesize($filename), 'filesize', $filename); |
509: | } |
510: | |
511: | |
512: | |
513: | |
514: | |
515: | |
516: | public static function type(string $filename): string |
517: | { |
518: | |
519: | return self::check(@filetype($filename), 'filetype', $filename); |
520: | } |
521: | |
522: | |
523: | |
524: | |
525: | |
526: | |
527: | |
528: | public static function writeContents(string $filename, $data, int $flags = 0): int |
529: | { |
530: | return self::check(@file_put_contents($filename, $data, $flags), 'file_put_contents', $filename); |
531: | } |
532: | |
533: | |
534: | |
535: | |
536: | |
537: | |
538: | |
539: | public static function checkEof($stream, $uri = null): void |
540: | { |
541: | $error = error_get_last(); |
542: | if (self::eof($stream, true)) { |
543: | return; |
544: | } |
545: | if ($error) { |
546: | throw new FilesystemErrorException($error['message']); |
547: | } |
548: | throw new FilesystemErrorException(sprintf( |
549: | 'Error reading from %s', |
550: | self::getStreamName($uri, $stream), |
551: | )); |
552: | } |
553: | |
554: | |
555: | |
556: | |
557: | |
558: | |
559: | |
560: | public static function close($stream, $uri = null): void |
561: | { |
562: | $uri = self::getStreamName($uri, $stream); |
563: | self::check(@fclose($stream), 'fclose', $uri); |
564: | } |
565: | |
566: | |
567: | |
568: | |
569: | |
570: | |
571: | public static function closePipe($pipe, ?string $command = null): int |
572: | { |
573: | $result = @pclose($pipe); |
574: | if ($result === -1) { |
575: | self::check(false, 'pclose', $command ?? '<pipe>'); |
576: | } |
577: | return $result; |
578: | } |
579: | |
580: | |
581: | |
582: | |
583: | |
584: | |
585: | public static function eof($stream, bool $returnFalseOnError = false): bool |
586: | { |
587: | error_clear_last(); |
588: | $result = @feof($stream); |
589: | if ($error = error_get_last()) { |
590: | if ($returnFalseOnError) { |
591: | return false; |
592: | } |
593: | throw new FilesystemErrorException($error['message']); |
594: | } |
595: | return $result; |
596: | } |
597: | |
598: | |
599: | |
600: | |
601: | |
602: | |
603: | |
604: | |
605: | |
606: | public static function getSeekableStream($stream, $uri = null) |
607: | { |
608: | if (self::isSeekableStream($stream)) { |
609: | return $stream; |
610: | } |
611: | $seekable = self::open('php://temp', 'r+'); |
612: | self::copy($stream, $seekable, $uri); |
613: | self::close($stream, $uri); |
614: | self::rewind($seekable); |
615: | return $seekable; |
616: | } |
617: | |
618: | |
619: | |
620: | |
621: | |
622: | |
623: | |
624: | public static function getStreamUri($stream): ?string |
625: | { |
626: | if (self::isStream($stream)) { |
627: | return stream_get_meta_data($stream)['uri'] ?? null; |
628: | } |
629: | return null; |
630: | } |
631: | |
632: | |
633: | |
634: | |
635: | |
636: | |
637: | |
638: | public static function isSeekableStream($value): bool |
639: | { |
640: | return self::isStream($value) |
641: | |
642: | && (stream_get_meta_data($value)['seekable'] ?? false); |
643: | } |
644: | |
645: | |
646: | |
647: | |
648: | |
649: | |
650: | |
651: | public static function isStream($value): bool |
652: | { |
653: | return is_resource($value) && get_resource_type($value) === 'stream'; |
654: | } |
655: | |
656: | |
657: | |
658: | |
659: | |
660: | |
661: | public static function open(string $filename, string $mode) |
662: | { |
663: | return self::check(@fopen($filename, $mode), 'fopen', $filename); |
664: | } |
665: | |
666: | |
667: | |
668: | |
669: | |
670: | |
671: | |
672: | |
673: | |
674: | |
675: | |
676: | |
677: | public static function maybeOpen($resource, string $mode, ?bool &$close, &$uri) |
678: | { |
679: | $close = false; |
680: | if (is_resource($resource)) { |
681: | if ($uri === null) { |
682: | $uri = self::getStreamUri($resource); |
683: | } |
684: | return $resource; |
685: | } |
686: | $uri = (string) $resource; |
687: | $close = true; |
688: | return self::open($uri, $mode); |
689: | } |
690: | |
691: | |
692: | |
693: | |
694: | |
695: | |
696: | public static function openPipe(string $command, string $mode) |
697: | { |
698: | return self::check(@popen($command, $mode), 'popen', $command); |
699: | } |
700: | |
701: | |
702: | |
703: | |
704: | |
705: | |
706: | |
707: | |
708: | public static function read($stream, int $length, $uri = null): string |
709: | { |
710: | return self::check(@fread($stream, $length), 'fread', $uri, $stream); |
711: | } |
712: | |
713: | |
714: | |
715: | |
716: | |
717: | |
718: | |
719: | |
720: | |
721: | |
722: | public static function readAll($stream, int $length, $uri = null): string |
723: | { |
724: | if ($length === 0) { |
725: | return ''; |
726: | } |
727: | $data = ''; |
728: | $dataLength = 0; |
729: | do { |
730: | |
731: | $unread = $length - $dataLength; |
732: | $result = self::read($stream, $unread, $uri); |
733: | if ($result === '') { |
734: | if (self::eof($stream)) { |
735: | break; |
736: | } |
737: | usleep(10000); |
738: | continue; |
739: | } |
740: | $data .= $result; |
741: | $dataLength += strlen($result); |
742: | if ($dataLength === $length) { |
743: | return $data; |
744: | } |
745: | |
746: | usleep(10000); |
747: | } while (true); |
748: | |
749: | throw new UnreadDataException(Inflect::format( |
750: | $unread, |
751: | 'Error reading from stream: expected {{#}} more {{#:byte}} from %s', |
752: | self::getStreamName($uri, $stream), |
753: | )); |
754: | } |
755: | |
756: | |
757: | |
758: | |
759: | |
760: | |
761: | |
762: | public static function readLine($stream, $uri = null): string |
763: | { |
764: | $line = @fgets($stream); |
765: | if ($line !== false) { |
766: | return $line; |
767: | } |
768: | self::checkEof($stream, $uri); |
769: | return ''; |
770: | } |
771: | |
772: | |
773: | |
774: | |
775: | |
776: | |
777: | |
778: | public static function rewind($stream, $uri = null): void |
779: | { |
780: | self::check(rewind($stream), 'rewind', $uri, $stream); |
781: | } |
782: | |
783: | |
784: | |
785: | |
786: | |
787: | |
788: | |
789: | public static function maybeRewind($stream, $uri = null): void |
790: | { |
791: | if (self::isSeekableStream($stream)) { |
792: | self::rewind($stream, $uri); |
793: | } |
794: | } |
795: | |
796: | |
797: | |
798: | |
799: | |
800: | |
801: | |
802: | |
803: | public static function seek($stream, int $offset, int $whence = \SEEK_SET, $uri = null): void |
804: | { |
805: | |
806: | if (@fseek($stream, $offset, $whence) === -1) { |
807: | self::check(false, 'fseek', $uri, $stream); |
808: | } |
809: | } |
810: | |
811: | |
812: | |
813: | |
814: | |
815: | |
816: | |
817: | |
818: | public static function maybeSeek($stream, int $offset, int $whence = \SEEK_SET, $uri = null): void |
819: | { |
820: | if (self::isSeekableStream($stream)) { |
821: | |
822: | self::seek($stream, $offset, $whence, $uri); |
823: | } |
824: | } |
825: | |
826: | |
827: | |
828: | |
829: | |
830: | |
831: | |
832: | |
833: | |
834: | |
835: | |
836: | |
837: | public static function select(?array &$read, ?array &$write, ?array &$except, ?int $seconds, ?int $microseconds = null): int |
838: | { |
839: | return self::check(@stream_select( |
840: | $read, |
841: | $write, |
842: | $except, |
843: | $seconds, |
844: | |
845: | $microseconds ?? 0, |
846: | ), 'stream_select'); |
847: | } |
848: | |
849: | |
850: | |
851: | |
852: | |
853: | |
854: | |
855: | public static function tell($stream, $uri = null): int |
856: | { |
857: | return self::check(@ftell($stream), 'ftell', $uri, $stream); |
858: | } |
859: | |
860: | |
861: | |
862: | |
863: | |
864: | |
865: | |
866: | |
867: | public static function truncate($stream, int $size = 0, $uri = null): void |
868: | { |
869: | self::seek($stream, 0, \SEEK_SET, $uri); |
870: | self::check(@ftruncate($stream, $size), 'ftruncate', $uri, $stream); |
871: | } |
872: | |
873: | |
874: | |
875: | |
876: | |
877: | |
878: | |
879: | |
880: | |
881: | public static function write($stream, string $data, ?int $length = null, $uri = null): int |
882: | { |
883: | $result = self::doWrite($stream, $data, $length, $unwritten, $uri); |
884: | if ($unwritten > 0) { |
885: | throw new UnwrittenDataException(Inflect::format( |
886: | $unwritten, |
887: | 'Error writing to stream: {{#}} {{#:byte}} not written to %s', |
888: | self::getStreamName($uri, $stream), |
889: | )); |
890: | } |
891: | return $result; |
892: | } |
893: | |
894: | |
895: | |
896: | |
897: | |
898: | |
899: | |
900: | |
901: | public static function writeAll($stream, string $data, ?int $length = null, $uri = null): int |
902: | { |
903: | if ($length !== null) { |
904: | $data = substr($data, 0, $length); |
905: | } |
906: | if ($data === '') { |
907: | return 0; |
908: | } |
909: | $result = 0; |
910: | do { |
911: | $result += self::maybeWrite($stream, $data, $data, null, $uri); |
912: | if ($data === '') { |
913: | return $result; |
914: | } |
915: | |
916: | |
917: | usleep(10000); |
918: | |
919: | } while (true); |
920: | } |
921: | |
922: | |
923: | |
924: | |
925: | |
926: | |
927: | |
928: | |
929: | |
930: | public static function maybeWrite($stream, string $data, ?string &$buffer, ?int $length = null, $uri = null): int |
931: | { |
932: | $result = self::doWrite($stream, $data, $length, $unwritten, $uri); |
933: | $buffer = (string) substr($data, $result); |
934: | return $result; |
935: | } |
936: | |
937: | |
938: | |
939: | |
940: | |
941: | |
942: | |
943: | private static function doWrite($stream, string $data, ?int $length, ?int &$unwritten, $uri): int |
944: | { |
945: | |
946: | if ($length === null) { |
947: | $length = strlen($data); |
948: | $expected = $length; |
949: | } else { |
950: | $expected = min($length, strlen($data)); |
951: | } |
952: | $result = @fwrite($stream, $data, $length); |
953: | self::check($result, 'fwrite', $uri, $stream); |
954: | |
955: | $unwritten = $expected - $result; |
956: | return $result; |
957: | } |
958: | |
959: | |
960: | |
961: | |
962: | |
963: | |
964: | |
965: | |
966: | |
967: | |
968: | |
969: | public static function writeCsvLine( |
970: | $stream, |
971: | array $fields, |
972: | string $separator = ',', |
973: | string $enclosure = '"', |
974: | string $eol = "\n", |
975: | $uri = null |
976: | ): int { |
977: | $special = $separator . $enclosure . "\n\r\t "; |
978: | foreach ($fields as &$field) { |
979: | if (strpbrk((string) $field, $special) !== false) { |
980: | $field = $enclosure |
981: | . str_replace($enclosure, $enclosure . $enclosure, (string) $field) |
982: | . $enclosure; |
983: | } |
984: | } |
985: | $line = implode($separator, $fields) . $eol; |
986: | return self::write($stream, $line, null, $uri); |
987: | } |
988: | |
989: | |
990: | |
991: | |
992: | |
993: | |
994: | |
995: | |
996: | |
997: | public static function copy($from, $to, $fromUri = null, $toUri = null): void |
998: | { |
999: | if (is_resource($from) && is_resource($to)) { |
1000: | $result = @stream_copy_to_stream($from, $to); |
1001: | $from = self::getStreamName($fromUri, $from); |
1002: | $to = self::getStreamName($toUri, $to); |
1003: | self::check($result, 'stream_copy_to_stream', null, null, $from, $to); |
1004: | } elseif (Test::isStringable($from) && Test::isStringable($to)) { |
1005: | $from = (string) $from; |
1006: | $to = (string) $to; |
1007: | self::check(@copy($from, $to), 'copy', null, null, $from, $to); |
1008: | } else { |
1009: | throw new InvalidArgumentException('$from and $to must both be Stringable|string or resource'); |
1010: | } |
1011: | } |
1012: | |
1013: | |
1014: | |
1015: | |
1016: | |
1017: | |
1018: | |
1019: | public static function getContents($resource, ?int $offset = null, $uri = null): string |
1020: | { |
1021: | if (is_resource($resource)) { |
1022: | return self::check(@stream_get_contents($resource, -1, $offset ?? -1), 'stream_get_contents', $uri, $resource); |
1023: | } |
1024: | $resource = (string) $resource; |
1025: | return self::check(@file_get_contents($resource, false, null, $offset ?? 0), 'file_get_contents', $resource); |
1026: | } |
1027: | |
1028: | |
1029: | |
1030: | |
1031: | |
1032: | |
1033: | |
1034: | |
1035: | public static function getCsv($resource, $uri = null): array |
1036: | { |
1037: | $handle = self::maybeOpen($resource, 'rb', $close, $uri); |
1038: | while (($row = @fgetcsv($handle, 0, ',', '"', '')) !== false) { |
1039: | |
1040: | $data[] = $row; |
1041: | } |
1042: | self::checkEof($handle, $uri); |
1043: | if ($close) { |
1044: | self::close($handle, $uri); |
1045: | } |
1046: | return $data ?? []; |
1047: | } |
1048: | |
1049: | |
1050: | |
1051: | |
1052: | |
1053: | |
1054: | |
1055: | |
1056: | |
1057: | |
1058: | |
1059: | |
1060: | |
1061: | |
1062: | public static function getEol($resource, $uri = null): ?string |
1063: | { |
1064: | $handle = self::maybeOpen($resource, 'r', $close, $uri); |
1065: | $line = self::readLine($handle, $uri); |
1066: | if ($close) { |
1067: | self::close($handle, $uri); |
1068: | } |
1069: | if ($line === '') { |
1070: | return null; |
1071: | } |
1072: | foreach (["\r\n", "\n", "\r"] as $eol) { |
1073: | if (substr($line, -strlen($eol)) === $eol) { |
1074: | return $eol; |
1075: | } |
1076: | } |
1077: | if (strpos($line, "\r") !== false) { |
1078: | return "\r"; |
1079: | } |
1080: | return null; |
1081: | } |
1082: | |
1083: | |
1084: | |
1085: | |
1086: | |
1087: | |
1088: | |
1089: | |
1090: | |
1091: | public static function getIndentation( |
1092: | $resource, |
1093: | ?Indentation $default = null, |
1094: | bool $alwaysGuessTabSize = false, |
1095: | $uri = null |
1096: | ): Indentation { |
1097: | return (new IndentationGuesser($default, $alwaysGuessTabSize)) |
1098: | ->guess($resource, $uri); |
1099: | } |
1100: | |
1101: | |
1102: | |
1103: | |
1104: | |
1105: | |
1106: | |
1107: | |
1108: | public static function getLines($resource, $uri = null): array |
1109: | { |
1110: | if (is_resource($resource)) { |
1111: | while (($line = @fgets($resource)) !== false) { |
1112: | $lines[] = $line; |
1113: | } |
1114: | self::checkEof($resource, $uri); |
1115: | return $lines ?? []; |
1116: | } |
1117: | $resource = (string) $resource; |
1118: | return self::check(@file($resource), 'file', $resource); |
1119: | } |
1120: | |
1121: | |
1122: | |
1123: | |
1124: | |
1125: | |
1126: | |
1127: | |
1128: | |
1129: | |
1130: | public static function hasPhp($resource, $uri = null): bool |
1131: | { |
1132: | $handle = self::maybeOpen($resource, 'r', $close, $uri); |
1133: | $line = self::readLine($handle, $uri); |
1134: | if ($line !== '' && substr($line, 0, 2) === '#!') { |
1135: | $line = self::readLine($handle, $uri); |
1136: | } |
1137: | if ($close) { |
1138: | self::close($handle, $uri); |
1139: | } |
1140: | if ($line === '') { |
1141: | return false; |
1142: | } |
1143: | return (bool) Regex::match('/^<\?(php\s|(?!php|xml\s))/', $line); |
1144: | } |
1145: | |
1146: | |
1147: | |
1148: | |
1149: | |
1150: | |
1151: | |
1152: | |
1153: | public static function stat($resource, $uri = null): array |
1154: | { |
1155: | if (is_resource($resource)) { |
1156: | return self::check(@fstat($resource), 'fstat', $uri, $resource); |
1157: | } |
1158: | $resource = (string) $resource; |
1159: | return self::check(@stat($resource), 'stat', $resource); |
1160: | } |
1161: | |
1162: | |
1163: | |
1164: | |
1165: | |
1166: | |
1167: | |
1168: | |
1169: | |
1170: | |
1171: | |
1172: | |
1173: | |
1174: | |
1175: | |
1176: | |
1177: | |
1178: | |
1179: | |
1180: | |
1181: | public static function writeCsv( |
1182: | $resource, |
1183: | iterable $data, |
1184: | bool $headerRow = true, |
1185: | $null = null, |
1186: | ?callable $callback = null, |
1187: | ?int &$count = null, |
1188: | string $eol = "\r\n", |
1189: | bool $utf16le = true, |
1190: | bool $bom = true, |
1191: | $uri = null |
1192: | ): void { |
1193: | $handle = self::maybeOpen($resource, 'wb', $close, $uri); |
1194: | if ($utf16le) { |
1195: | if (!extension_loaded('iconv')) { |
1196: | |
1197: | throw new InvalidRuntimeConfigurationException( |
1198: | "'iconv' extension required for UTF-16LE encoding" |
1199: | ); |
1200: | |
1201: | } |
1202: | $result = @stream_filter_append($handle, 'convert.iconv.UTF-8.UTF-16LE', \STREAM_FILTER_WRITE); |
1203: | $filter = self::check($result, 'stream_filter_append', $uri, $handle); |
1204: | } |
1205: | if ($bom) { |
1206: | self::write($handle, "\u{FEFF}", null, $uri); |
1207: | } |
1208: | $count = 0; |
1209: | foreach ($data as $entry) { |
1210: | if ($callback) { |
1211: | $entry = $callback($entry, $count); |
1212: | } |
1213: | |
1214: | $row = Arr::toScalars($entry, $null); |
1215: | if (!$count && $headerRow) { |
1216: | self::writeCsvLine($handle, array_keys($row), ',', '"', $eol, $uri); |
1217: | } |
1218: | self::writeCsvLine($handle, $row, ',', '"', $eol, $uri); |
1219: | $count++; |
1220: | } |
1221: | if ($close) { |
1222: | self::close($handle, $uri); |
1223: | } elseif ($utf16le) { |
1224: | self::check(@stream_filter_remove($filter), 'stream_filter_remove', $uri, $handle); |
1225: | } |
1226: | } |
1227: | |
1228: | |
1229: | |
1230: | |
1231: | |
1232: | |
1233: | |
1234: | |
1235: | |
1236: | private static function check($result, string $function, $uri = null, $stream = null, string ...$args) |
1237: | { |
1238: | if ($result !== false) { |
1239: | return $result; |
1240: | } |
1241: | $error = error_get_last(); |
1242: | if ($error) { |
1243: | throw new FilesystemErrorException($error['message']); |
1244: | } |
1245: | if (func_num_args() < 3) { |
1246: | throw new FilesystemErrorException(sprintf( |
1247: | 'Error calling %s()', |
1248: | $function, |
1249: | )); |
1250: | } |
1251: | throw new FilesystemErrorException(sprintf( |
1252: | 'Error calling %s() with %s', |
1253: | $function, |
1254: | $args |
1255: | ? implode(', ', $args) |
1256: | : self::getStreamName($uri, $stream), |
1257: | )); |
1258: | } |
1259: | |
1260: | |
1261: | |
1262: | |
1263: | |
1264: | private static function getStreamName($uri, $stream): string |
1265: | { |
1266: | if ($uri !== null) { |
1267: | return (string) $uri; |
1268: | } |
1269: | if ($stream !== null) { |
1270: | $uri = self::getStreamUri($stream); |
1271: | } |
1272: | return $uri ?? '<stream>'; |
1273: | } |
1274: | } |
1275: | |