Passed
Push — master ( 089fbf...f223f6 )
by Sebastian
04:05
created

FileHelper::saveFile()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 63
Code Lines 34

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 34
c 3
b 0
f 0
dl 0
loc 63
rs 8.7537
cc 6
nc 8
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * File containing the {@see AppUtils\FileHelper} class.
4
 * 
5
 * @package Application Utils
6
 * @subpackage FileHelper
7
 * @see FileHelper
8
 */
9
10
namespace AppUtils;
11
12
/**
13
 * Collection of file system related methods.
14
 * 
15
 * @package Application Utils
16
 * @subpackage FileHelper
17
 * @author Sebastian Mordziol <[email protected]>
18
 */
19
class FileHelper
20
{
21
    const ERROR_CANNOT_FIND_JSON_FILE = 340001;
22
    const ERROR_JSON_FILE_CANNOT_BE_READ = 340002;
23
    const ERROR_CANNOT_DECODE_JSON_FILE = 340003;
24
    const ERROR_CANNOT_SEND_MISSING_FILE = 340004;
25
    const ERROR_JSON_ENCODE_ERROR = 340005;
26
    const ERROR_CANNOT_OPEN_URL = 340008;
27
    const ERROR_CANNOT_CREATE_FOLDER = 340009;
28
    const ERROR_FILE_NOT_READABLE = 340010;
29
    const ERROR_CANNOT_COPY_FILE = 340011;
30
    const ERROR_CANNOT_DELETE_FILE = 340012;
31
    const ERROR_FIND_SUBFOLDERS_FOLDER_DOES_NOT_EXIST = 340014;
32
    const ERROR_UNKNOWN_FILE_MIME_TYPE = 340015;
33
    const ERROR_SERIALIZED_FILE_CANNOT_BE_READ = 340017;
34
    const ERROR_SERIALIZED_FILE_UNSERIALZE_FAILED = 340018;
35
    const ERROR_UNSUPPORTED_OS_CLI_COMMAND = 340019;
36
    const ERROR_SOURCE_FILE_NOT_FOUND = 340020;
37
    const ERROR_SOURCE_FILE_NOT_READABLE = 340021;
38
    const ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE = 340022;
39
    const ERROR_SAVE_FOLDER_NOT_WRITABLE = 340023;
40
    const ERROR_SAVE_FILE_NOT_WRITABLE = 340024;
41
    const ERROR_SAVE_FILE_WRITE_FAILED = 340025;
42
    const ERROR_FILE_DOES_NOT_EXIST = 340026;
43
    const ERROR_CANNOT_OPEN_FILE_TO_READ_LINES = 340027;
44
    const ERROR_CANNOT_READ_FILE_CONTENTS = 340028;
45
    const ERROR_PARSING_CSV = 340029;
46
    const ERROR_CURL_INIT_FAILED = 340030;
47
    const ERROR_CURL_OUTPUT_NOT_STRING = 340031;
48
    const ERROR_CANNOT_OPEN_FILE_TO_DETECT_BOM = 340032;
49
    const ERROR_FOLDER_DOES_NOT_EXIST = 340033;
50
    const ERROR_PATH_IS_NOT_A_FOLDER = 340034;
51
    const ERROR_CANNOT_WRITE_TO_FOLDER = 340035;
52
    
53
   /**
54
    * Opens a serialized file and returns the unserialized data.
55
    * 
56
    * @param string $file
57
    * @throws FileHelper_Exception
58
    * @return array
59
    * @deprecated Use parseSerializedFile() instead.
60
    * @see FileHelper::parseSerializedFile()
61
    */
62
    public static function openUnserialized(string $file) : array
63
    {
64
        return self::parseSerializedFile($file);
65
    }
66
67
   /**
68
    * Opens a serialized file and returns the unserialized data.
69
    *
70
    * @param string $file
71
    * @throws FileHelper_Exception
72
    * @return array
73
    * @see FileHelper::parseSerializedFile()
74
    * 
75
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
76
    * @see FileHelper::ERROR_SERIALIZED_FILE_CANNOT_BE_READ
77
    * @see FileHelper::ERROR_SERIALIZED_FILE_UNSERIALZE_FAILED
78
    */
79
    public static function parseSerializedFile(string $file)
80
    {
81
        self::requireFileExists($file);
82
        
83
        $contents = file_get_contents($file);
84
        
85
        if($contents === false) 
86
        {
87
            throw new FileHelper_Exception(
88
                'Cannot load serialized content from file.',
89
                sprintf(
90
                    'Tried reading file contents at [%s].',
91
                    $file
92
                ),
93
                self::ERROR_SERIALIZED_FILE_CANNOT_BE_READ
94
            );
95
        }
96
        
97
        $result = @unserialize($contents);
98
        
99
        if($result !== false) {
100
            return $result;
101
        }
102
        
103
        throw new FileHelper_Exception(
104
            'Cannot unserialize the file contents.',
105
            sprintf(
106
                'Tried unserializing the data from file at [%s].',
107
                $file
108
            ),
109
            self::ERROR_SERIALIZED_FILE_UNSERIALZE_FAILED
110
        );
111
    }
112
    
113
    public static function deleteTree($rootFolder)
114
    {
115
        if(!file_exists($rootFolder)) {
116
            return true;
117
        }
118
        
119
        $d = new \DirectoryIterator($rootFolder);
120
        foreach ($d as $item) {
121
            if ($item->isDot()) {
122
                continue;
123
            }
124
125
            $itemPath = $item->getRealPath();
126
            if (!is_readable($itemPath)) {
127
                return false;
128
            }
129
130
            if ($item->isDir()) {
131
                if (!FileHelper::deleteTree($itemPath)) {
132
                    return false;
133
                }
134
                continue;
135
            }
136
137
            if ($item->isFile()) {
138
                if (!unlink($itemPath)) {
139
                    return false;
140
                }
141
            }
142
        }
143
144
        return rmdir($rootFolder);
145
    }
146
    
147
   /**
148
    * Create a folder, if it does not exist yet.
149
    *  
150
    * @param string $path
151
    * @throws FileHelper_Exception
152
    * @see FileHelper::ERROR_CANNOT_CREATE_FOLDER
153
    */
154
    public static function createFolder($path)
155
    {
156
        if(is_dir($path) || mkdir($path, 0777, true)) {
157
            return;
158
        }
159
        
160
        throw new FileHelper_Exception(
161
            sprintf('Could not create target folder [%s].', basename($path)),
162
            sprintf('Tried to create the folder in path [%s].', $path),
163
            self::ERROR_CANNOT_CREATE_FOLDER
164
        );
165
    }
166
167
    public static function copyTree($source, $target)
168
    {
169
        self::createFolder($target);
170
171
        $d = new \DirectoryIterator($source);
172
        foreach ($d as $item) 
173
        {
174
            if ($item->isDot()) {
175
                continue;
176
            }
177
178
            $itemPath = $item->getRealPath();
179
            if (!is_readable($itemPath)) {
180
                throw new FileHelper_Exception(
181
                    'Source file is not readable',
182
                    sprintf('The file [%s] cannot be accessed for reading.', $itemPath),
183
                    self::ERROR_FILE_NOT_READABLE
184
                );
185
            }
186
            
187
            $baseName = basename($itemPath);
188
189
            if ($item->isDir()) 
190
            {
191
                FileHelper::copyTree(str_replace('\\', '/', $itemPath), $target . '/' . $baseName);
192
            } 
193
            else if($item->isFile()) 
194
            {
195
                self::copyFile($itemPath, $target . '/' . $baseName);
196
            }
197
        }
198
    }
199
    
200
   /**
201
    * Copies a file to the target location. Includes checks
202
    * for most error sources, like the source file not being
203
    * readable. Automatically creates the target folder if it
204
    * does not exist yet.
205
    * 
206
    * @param string $sourcePath
207
    * @param string $targetPath
208
    * @throws FileHelper_Exception
209
    * 
210
    * @see FileHelper::ERROR_CANNOT_CREATE_FOLDER
211
    * @see FileHelper::ERROR_SOURCE_FILE_NOT_FOUND
212
    * @see FileHelper::ERROR_SOURCE_FILE_NOT_READABLE
213
    * @see FileHelper::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE
214
    * @see FileHelper::ERROR_CANNOT_COPY_FILE
215
    */
216
    public static function copyFile($sourcePath, $targetPath)
217
    {
218
        self::requireFileExists($sourcePath, self::ERROR_SOURCE_FILE_NOT_FOUND);
219
        
220
        if(!is_readable($sourcePath))
221
        {
222
            throw new FileHelper_Exception(
223
                sprintf('Source file [%s] to copy is not readable.', basename($sourcePath)),
224
                sprintf(
225
                    'Tried copying from path [%s].',
226
                    $sourcePath
227
                ),
228
                self::ERROR_SOURCE_FILE_NOT_READABLE
229
            );
230
        }
231
        
232
        $targetFolder = dirname($targetPath);
233
        
234
        if(!file_exists($targetFolder))
235
        {
236
            self::createFolder($targetFolder);
237
        }
238
        else if(!is_writable($targetFolder)) 
239
        {
240
            throw new FileHelper_Exception(
241
                sprintf('Target folder [%s] is not writable.', basename($targetFolder)),
242
                sprintf(
243
                    'Tried copying to target folder [%s].',
244
                    $targetFolder
245
                ),
246
                self::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE
247
            );
248
        }
249
        
250
        if(copy($sourcePath, $targetPath)) {
251
            return;
252
        }
253
        
254
        throw new FileHelper_Exception(
255
            sprintf('Cannot copy file [%s].', basename($sourcePath)),
256
            sprintf(
257
                'The file [%s] could not be copied from [%s] to [%s].',
258
                basename($sourcePath),
259
                $sourcePath,
260
                $targetPath
261
            ),
262
            self::ERROR_CANNOT_COPY_FILE
263
        );
264
    }
265
    
266
   /**
267
    * Deletes the target file. Ignored if it cannot be found,
268
    * and throws an exception if it fails.
269
    * 
270
    * @param string $filePath
271
    * @throws FileHelper_Exception
272
    * 
273
    * @see FileHelper::ERROR_CANNOT_DELETE_FILE
274
    */
275
    public static function deleteFile(string $filePath) : void
276
    {
277
        if(!file_exists($filePath)) {
278
            return;
279
        }
280
        
281
        if(unlink($filePath)) {
282
            return;
283
        }
284
        
285
        throw new FileHelper_Exception(
286
            sprintf('Cannot delete file [%s].', basename($filePath)),
287
            sprintf(
288
                'The file [%s] cannot be deleted.',
289
                $filePath
290
            ),
291
            self::ERROR_CANNOT_DELETE_FILE
292
        );
293
    }
294
295
    /**
296
    * Creates a new CSV parser instance and returns it.
297
    * 
298
    * @param string $delimiter
299
    * @param string $enclosure
300
    * @param string $escape
301
    * @param bool $heading
302
    * @return \parseCSV
303
    * @todo Move this to the CSV helper.
304
    */
305
    public static function createCSVParser(string $delimiter = ';', string $enclosure = '"', string $escape = '\\', bool $heading=false) : \parseCSV
306
    {
307
        if($delimiter==='') { $delimiter = ';'; }
308
        if($enclosure==='') { $enclosure = '"'; }
309
        
310
        $parser = new \parseCSV(null, null, null, array());
311
312
        $parser->delimiter = $delimiter;
313
        $parser->enclosure = $enclosure;
314
        $parser->heading = $heading;
315
        
316
        return $parser;
317
    }
318
319
   /**
320
    * Parses all lines in the specified string and returns an
321
    * indexed array with all csv values in each line.
322
    *
323
    * @param string $csv
324
    * @param string $delimiter
325
    * @param string $enclosure
326
    * @param string $escape
327
    * @param bool $heading
328
    * @return array
329
    * @throws FileHelper_Exception
330
    * 
331
    * @todo Move this to the CSVHelper.
332
    *
333
    * @see parseCSVFile()
334
    * @see FileHelper::ERROR_PARSING_CSV
335
    */
336
    public static function parseCSVString(string $csv, string $delimiter = ';', string $enclosure = '"', string $escape = '\\', bool $heading=false) : array
337
    {
338
        $parser = self::createCSVParser($delimiter, $enclosure, $escape, $heading);
339
        $result = $parser->parse_string(/** @scrutinizer ignore-type */ $csv);
340
        if(is_array($result)) {
341
            return $result;
342
        }
343
        
344
        throw new FileHelper_Exception(
345
            'Could not parse CSV string, possible formatting error.',
346
            'The parseCSV library returned an error, but exact details are not available.',
347
            self::ERROR_PARSING_CSV
348
        );
349
    }
350
351
    /**
352
     * Parses all lines in the specified file and returns an
353
     * indexed array with all csv values in each line.
354
     *
355
     * @param string $filePath
356
     * @param string $delimiter 
357
     * @param string $enclosure The character to use to quote literal strings
358
     * @param string $escape The character to use to escape special characters.
359
     * @param bool $heading Whether to include headings.
360
     * @return array
361
     * @throws FileHelper_Exception
362
     * 
363
     * @todo Move this to the CSVHelper.
364
     * 
365
     * @see parseCSVString()
366
     * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
367
     * @see FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
368
     */
369
    public static function parseCSVFile(string $filePath, string $delimiter = ';', string $enclosure = '"', string $escape = '\\', bool $heading=false) : array
370
    {
371
        $content = self::readContents($filePath);
372
373
        return self::parseCSVString($content, $delimiter, $enclosure, $escape, $heading);
374
    }
375
376
    /**
377
     * Detects the mime type for the specified file name/path.
378
     * Returns null if it is not a known file extension.
379
     *
380
     * @param string $fileName
381
     * @return string|NULL
382
     */
383
    public static function detectMimeType(string $fileName) : ?string
384
    {
385
        $ext = self::getExtension($fileName);
386
        if(empty($ext)) {
387
            return null;
388
        }
389
390
        return FileHelper_MimeTypes::getMime($ext);
391
    }
392
393
    /**
394
     * Detects the mime type of the target file automatically,
395
     * sends the required headers to trigger a download and
396
     * outputs the file. Returns false if the mime type could
397
     * not be determined.
398
     * 
399
     * @param string $filePath
400
     * @param string|null $fileName The name of the file for the client.
401
     * @param bool $asAttachment Whether to force the client to download the file.
402
     * @throws FileHelper_Exception
403
     * 
404
     * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
405
     * @see FileHelper::ERROR_UNKNOWN_FILE_MIME_TYPE
406
     */
407
    public static function sendFile(string $filePath, $fileName = null, bool $asAttachment=true)
408
    {
409
        self::requireFileExists($filePath);
410
        
411
        if(empty($fileName)) {
412
            $fileName = basename($filePath);
413
        }
414
415
        $mime = self::detectMimeType($filePath);
416
        if (!$mime) {
417
            throw new FileHelper_Exception(
418
                'Unknown file mime type',
419
                sprintf(
420
                    'Could not determine mime type for file name [%s].',
421
                    basename($filePath)
422
                ),
423
                self::ERROR_UNKNOWN_FILE_MIME_TYPE
424
            );
425
        }
426
        
427
        header("Cache-Control: public", true);
428
        header("Content-Description: File Transfer", true);
429
        header("Content-Type: " . $mime, true);
430
431
        $disposition = 'inline';
432
        if($asAttachment) {
433
            $disposition = 'attachment';
434
        }
435
        
436
        header(sprintf(
437
            "Content-Disposition: %s; filename=%s",
438
            $disposition,
439
            '"'.$fileName.'"'
440
        ), true);
441
        
442
        readfile($filePath);
443
    }
444
445
    /**
446
     * Uses cURL to download the contents of the specified URL,
447
     * returns the content.
448
     *
449
     * @param string $url
450
     * @throws FileHelper_Exception
451
     * @return string
452
     * 
453
     * @see FileHelper::ERROR_CANNOT_OPEN_URL
454
     */
455
    public static function downloadFile($url)
456
    {
457
        requireCURL();
458
        
459
        $ch = curl_init();
460
        if(!is_resource($ch)) 
461
        {
462
            throw new FileHelper_Exception(
463
                'Could not initialize a new cURL instance.',
464
                'Calling curl_init returned false. Additional information is not available.',
465
                self::ERROR_CURL_INIT_FAILED
466
            );
467
        }
468
        
469
        curl_setopt($ch, CURLOPT_URL, $url);
470
        curl_setopt($ch, CURLOPT_REFERER, $url);
471
        curl_setopt($ch, CURLOPT_USERAGENT, "Google Chrome/1.0");
472
        curl_setopt($ch, CURLOPT_HEADER, 0);
473
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
474
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
475
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
476
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
477
        curl_setopt($ch, CURLOPT_TIMEOUT, 100000);
478
        
479
        $output = curl_exec($ch);
480
481
        if($output === false) {
482
            throw new FileHelper_Exception(
483
                'Unable to open URL',
484
                sprintf(
485
                    'Tried accessing URL "%1$s" using cURL, but the request failed. cURL error: %2$s',
486
                    $url,
487
                    curl_error($ch)
488
                ),
489
                self::ERROR_CANNOT_OPEN_URL
490
            );
491
        }
492
493
        curl_close($ch);
494
495
        if(is_string($output)) 
496
        {
497
            return $output;
498
        }
499
        
500
        throw new FileHelper_Exception(
501
            'Unexpected cURL output.',
502
            'The cURL output is not a string, although the CURLOPT_RETURNTRANSFER option is set.',
503
            self::ERROR_CURL_OUTPUT_NOT_STRING
504
        );
505
    }
506
    
507
   /**
508
    * Verifies whether the target file is a PHP file. The path
509
    * to the file can be a path to a file as a string, or a 
510
    * DirectoryIterator object instance.
511
    * 
512
    * @param string|\DirectoryIterator $pathOrDirIterator
513
    * @return boolean
514
    */
515
    public static function isPHPFile($pathOrDirIterator)
516
    {
517
    	if(self::getExtension($pathOrDirIterator) == 'php') {
518
    		return true;
519
    	}
520
    	
521
    	return false;
522
    }
523
    
524
   /**
525
    * Retrieves the extension of the specified file. Can be a path
526
    * to a file as a string, or a DirectoryIterator object instance.
527
    * 
528
    * @param string|\DirectoryIterator $pathOrDirIterator
529
    * @param bool $lowercase
530
    * @return string
531
    */
532
    public static function getExtension($pathOrDirIterator, bool $lowercase = true) : string
533
    {
534
        if($pathOrDirIterator instanceof \DirectoryIterator) {
535
            $filename = $pathOrDirIterator->getFilename();
536
        } else {
537
            $filename = basename($pathOrDirIterator);
538
        }
539
         
540
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
541
        if($lowercase) {
542
        	$ext = mb_strtolower($ext);
543
        }
544
        
545
        return $ext;
546
    }
547
    
548
   /**
549
    * Retrieves the file name from a path, with or without extension.
550
    * The path to the file can be a string, or a DirectoryIterator object
551
    * instance.
552
    * 
553
    * In case of folders, behaves like the pathinfo function: returns
554
    * the name of the folder.
555
    * 
556
    * @param string|\DirectoryIterator $pathOrDirIterator
557
    * @param bool $extension
558
    * @return string
559
    */
560
    public static function getFilename($pathOrDirIterator, $extension = true)
561
    {
562
        $path = $pathOrDirIterator;
563
    	if($pathOrDirIterator instanceof \DirectoryIterator) {
564
    		$path = $pathOrDirIterator->getFilename();
565
    	}
566
    	
567
    	$path = self::normalizePath($path);
568
    	
569
    	if(!$extension) {
570
    	    return pathinfo($path, PATHINFO_FILENAME);
571
    	}
572
    	
573
    	return pathinfo($path, PATHINFO_BASENAME); 
574
    }
575
   
576
   /**
577
    * Tries to read the contents of the target file and
578
    * treat it as JSON to return the decoded JSON data.
579
    * 
580
    * @param string $file
581
    * @throws FileHelper_Exception
582
    * @return array
583
    * 
584
    * @see FileHelper::ERROR_CANNOT_FIND_JSON_FILE
585
    * @see FileHelper::ERROR_CANNOT_DECODE_JSON_FILE
586
    */ 
587
    public static function parseJSONFile(string $file, $targetEncoding=null, $sourceEncoding=null)
588
    {
589
        self::requireFileExists($file, self::ERROR_CANNOT_FIND_JSON_FILE);
590
        
591
        $content = file_get_contents($file);
592
        if(!$content) {
593
            throw new FileHelper_Exception(
594
                'Cannot get file contents',
595
                sprintf(
596
                    'The file [%s] exists on disk, but its contents cannot be read.',
597
                    $file    
598
                ),
599
                self::ERROR_JSON_FILE_CANNOT_BE_READ
600
            );
601
        }
602
        
603
        if(isset($targetEncoding)) {
604
            $content = mb_convert_encoding($content, $targetEncoding, $sourceEncoding);
605
        }
606
        
607
        $json = json_decode($content, true);
608
        if($json === false || $json === NULL) {
609
            throw new FileHelper_Exception(
610
                'Cannot decode json data',
611
                sprintf(
612
                    'Loaded the contents of file [%s] successfully, but decoding it as JSON failed.',
613
                    $file    
614
                ),
615
                self::ERROR_CANNOT_DECODE_JSON_FILE
616
            );
617
        }
618
        
619
        return $json;
620
    }
621
    
622
   /**
623
    * Corrects common formatting mistakes when users enter
624
    * file names, like too many spaces, dots and the like.
625
    * 
626
    * NOTE: if the file name contains a path, the path is
627
    * stripped, leaving only the file name.
628
    * 
629
    * @param string $name
630
    * @return string
631
    */
632
    public static function fixFileName(string $name) : string
633
    {
634
        $name = trim($name);
635
        $name = self::normalizePath($name);
636
        $name = basename($name);
637
        
638
        $replaces = array(
639
            "\t" => ' ',
640
            "\r" => ' ',
641
            "\n" => ' ',
642
            ' .' => '.',
643
            '. ' => '.',
644
        );
645
        
646
        $name = str_replace(array_keys($replaces), array_values($replaces), $name);
647
        
648
        while(strstr($name, '  ')) {
649
            $name = str_replace('  ', ' ', $name);
650
        }
651
652
        $name = str_replace(array_keys($replaces), array_values($replaces), $name);
653
        
654
        while(strstr($name, '..')) {
655
            $name = str_replace('..', '.', $name);
656
        }
657
        
658
        return $name;
659
    }
660
    
661
   /**
662
    * Creates an instance of the file finder, which is an easier
663
    * alternative to the other manual findFile methods, since all
664
    * options can be set by chaining.
665
    * 
666
    * @param string $path
667
    * @return FileHelper_FileFinder
668
    */
669
    public static function createFileFinder(string $path) : FileHelper_FileFinder
670
    {
671
        return new FileHelper_FileFinder($path);
672
    }
673
    
674
   /**
675
    * Searches for all HTML files in the target folder.
676
    * 
677
    * NOTE: This method only exists for backwards compatibility.
678
    * Use the `createFileFinder()` method instead, which offers
679
    * an object oriented interface that is much easier to use.
680
    * 
681
    * @param string $targetFolder
682
    * @param array $options
683
    * @return array An indexed array with files.
684
    * @see FileHelper::createFileFinder()
685
    */
686
    public static function findHTMLFiles(string $targetFolder, array $options=array()) : array
687
    {
688
        return self::findFiles($targetFolder, array('html'), $options);
689
    }
690
691
   /**
692
    * Searches for all PHP files in the target folder.
693
    * 
694
    * NOTE: This method only exists for backwards compatibility.
695
    * Use the `createFileFinder()` method instead, which offers
696
    * an object oriented interface that is much easier to use.
697
    * 
698
    * @param string $targetFolder
699
    * @param array $options
700
    * @return array An indexed array of PHP files.
701
    * @see FileHelper::createFileFinder()
702
    */
703
    public static function findPHPFiles(string $targetFolder, array $options=array()) : array
704
    {
705
        return self::findFiles($targetFolder, array('php'), $options);
706
    }
707
    
708
   /**
709
    * Finds files according to the specified options.
710
    * 
711
    * NOTE: This method only exists for backwards compatibility.
712
    * Use the `createFileFinder()` method instead, which offers
713
    * an object oriented interface that is much easier to use.
714
    *  
715
    * @param string $targetFolder
716
    * @param array $extensions
717
    * @param array $options
718
    * @param array $files
719
    * @throws FileHelper_Exception
720
    * @return array
721
    * @see FileHelper::createFileFinder()
722
    */
723
    public static function findFiles(string $targetFolder, array $extensions=array(), array $options=array(), array $files=array()) : array
724
    {
725
        $finder = self::createFileFinder($targetFolder);
726
727
        $finder->setPathmodeStrip();
728
        
729
        if(isset($options['relative-path']) && $options['relative-path'] === true) 
730
        {
731
            $finder->setPathmodeRelative();
732
        } 
733
        else if(isset($options['absolute-path']) && $options['absolute-path'] === true)
734
        {
735
            $finder->setPathmodeAbsolute();
736
        }
737
        
738
        if(isset($options['strip-extension'])) 
739
        {
740
            $finder->stripExtensions();
741
        }
742
        
743
        $finder->setOptions($options);
744
        
745
        return $finder->getAll();
746
    }
747
748
   /**
749
    * Removes the extension from the specified path or file name,
750
    * if any, and returns the name without the extension.
751
    * 
752
    * @param string $filename
753
    * @param bool $keepPath Whether to keep the path component, if any. Default PHP pathinfo behavior is not to.
754
    * @return string
755
    */
756
    public static function removeExtension(string $filename, bool $keepPath=false) : string
757
    {
758
        // normalize paths to allow windows style slashes even on nix servers
759
        $filename = self::normalizePath($filename);
760
        
761
        if(!$keepPath) 
762
        {
763
            return pathinfo($filename, PATHINFO_FILENAME);
764
        }
765
        
766
        $parts = explode('/', $filename);
767
        
768
        $file = self::removeExtension(array_pop($parts));
769
        
770
        $parts[] = $file;
771
        
772
        return implode('/', $parts);
773
    }
774
    
775
   /**
776
    * Detects the UTF BOM in the target file, if any. Returns
777
    * the encoding matching the BOM, which can be any of the
778
    * following:
779
    * 
780
    * <ul>
781
    * <li>UTF32-BE</li>
782
    * <li>UTF32-LE</li>
783
    * <li>UTF16-BE</li>
784
    * <li>UTF16-LE</li>
785
    * <li>UTF8</li>
786
    * </ul>
787
    * 
788
    * @param string $filename
789
    * @return string|NULL
790
    */
791
    public static function detectUTFBom(string $filename) : ?string
792
    {
793
        $fp = fopen($filename, 'r');
794
        if($fp === false) 
795
        {
796
            throw new FileHelper_Exception(
797
                'Cannot open file for reading',
798
                sprintf('Tried opening file [%s] in read mode.', $filename),
799
                self::ERROR_CANNOT_OPEN_FILE_TO_DETECT_BOM
800
            );
801
        }
802
        
803
        $text = fread($fp, 20);
804
        
805
        fclose($fp);
806
807
        $boms = self::getUTFBOMs();
808
        
809
        foreach($boms as $bom => $value) 
810
        {
811
            $length = mb_strlen($value);
812
            if(mb_substr($text, 0, $length) == $value) {
813
                return $bom;
814
            }
815
        }
816
        
817
        return null;
818
    }    
819
    
820
    protected static $utfBoms;
821
    
822
   /**
823
    * Retrieves a list of all UTF byte order mark character
824
    * sequences, as an assocative array with UTF encoding => bom sequence
825
    * pairs.
826
    * 
827
    * @return array
828
    */
829
    public static function getUTFBOMs()
830
    {
831
        if(!isset(self::$utfBoms)) {
832
            self::$utfBoms = array(
833
                'UTF32-BE' => chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF),
834
                'UTF32-LE' => chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00),
835
                'UTF16-BE' => chr(0xFE) . chr(0xFF),
836
                'UTF16-LE' => chr(0xFF) . chr(0xFE),
837
                'UTF8' => chr(0xEF) . chr(0xBB) . chr(0xBF)
838
            );
839
        }
840
        
841
        return self::$utfBoms;
842
    }
843
    
844
   /**
845
    * Checks whether the specified encoding is a valid
846
    * unicode encoding, for example "UTF16-LE" or "UTF8".
847
    * Also accounts for alternate way to write the, like
848
    * "UTF-8", and omitting little/big endian suffixes.
849
    * 
850
    * @param string $encoding
851
    * @return boolean
852
    */
853
    public static function isValidUnicodeEncoding(string $encoding) : bool
854
    {
855
        $encodings = self::getKnownUnicodeEncodings();
856
857
        $keep = array();
858
        foreach($encodings as $string) 
859
        {
860
            $withHyphen = str_replace('UTF', 'UTF-', $string);
861
            
862
            $keep[] = $string;
863
            $keep[] = $withHyphen; 
864
            $keep[] = str_replace(array('-BE', '-LE'), '', $string);
865
            $keep[] = str_replace(array('-BE', '-LE'), '', $withHyphen);
866
        }
867
        
868
        return in_array($encoding, $keep);
869
    }
870
    
871
   /**
872
    * Retrieves a list of all known unicode file encodings.
873
    * @return array
874
    */
875
    public static function getKnownUnicodeEncodings()
876
    {
877
        return array_keys(self::getUTFBOMs());
878
    }
879
    
880
   /**
881
    * Normalizes the slash style in a file or folder path,
882
    * by replacing any antislashes with forward slashes.
883
    * 
884
    * @param string $path
885
    * @return string
886
    */
887
    public static function normalizePath(string $path) : string
888
    {
889
        return str_replace(array('\\', '//'), array('/', '/'), $path);
890
    }
891
    
892
   /**
893
    * Saves the specified data to a file, JSON encoded.
894
    * 
895
    * @param mixed $data
896
    * @param string $file
897
    * @param bool $pretty
898
    * @throws FileHelper_Exception
899
    * 
900
    * @see FileHelper::ERROR_JSON_ENCODE_ERROR
901
    * @see FileHelper::ERROR_SAVE_FOLDER_NOT_WRITABLE
902
    * @see FileHelper::ERROR_SAVE_FILE_NOT_WRITABLE
903
    * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
904
    */
905
    public static function saveAsJSON($data, string $file, bool $pretty=false)
906
    {
907
        $options = null;
908
        if($pretty) {
909
            $options = JSON_PRETTY_PRINT;
910
        }
911
        
912
        $json = json_encode($data, $options);
913
        
914
        if($json===false) 
915
        {
916
            $errorCode = json_last_error();
917
            
918
            throw new FileHelper_Exception(
919
                'An error occurred while encdoding a data set to JSON. Native error message: ['.json_last_error_msg().'].', 
920
                'JSON error code: '.$errorCode,
921
                self::ERROR_JSON_ENCODE_ERROR
922
            ); 
923
        }
924
        
925
        self::saveFile($file, $json);
926
    }
927
   
928
   /**
929
    * Saves the specified content to the target file, creating
930
    * the file and the folder as necessary.
931
    * 
932
    * @param string $filePath
933
    * @param string $content
934
    * @throws FileHelper_Exception
935
    * 
936
    * @see FileHelper::ERROR_SAVE_FOLDER_NOT_WRITABLE
937
    * @see FileHelper::ERROR_SAVE_FILE_NOT_WRITABLE
938
    * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
939
    */
940
    public static function saveFile(string $filePath, string $content='') : void
941
    {
942
        $filePath = self::normalizePath($filePath);
943
        
944
        // target file already exists
945
        if(file_exists($filePath))
946
        {
947
            if(!is_writable($filePath))
948
            {
949
                throw new FileHelper_Exception(
950
                    sprintf('Cannot save file: target file [%s] exists, but is not writable.', basename($filePath)),
951
                    sprintf(
952
                        'Tried accessing the file in path [%s].',
953
                        $filePath
954
                    ),
955
                    self::ERROR_SAVE_FILE_NOT_WRITABLE
956
                );
957
            }
958
        }
959
        // target file does not exist yet
960
        else
961
        {
962
            $targetFolder = dirname($filePath);
963
            
964
            // create the folder as needed
965
            self::createFolder($targetFolder);
966
            
967
            if(!is_writable($targetFolder)) 
968
            {
969
                throw new FileHelper_Exception(
970
                    sprintf('Cannot save file: target folder [%s] is not writable.', basename($targetFolder)),
971
                    sprintf(
972
                        'Tried accessing the folder in path [%s].',
973
                        $targetFolder
974
                    ),
975
                    self::ERROR_SAVE_FOLDER_NOT_WRITABLE
976
                );
977
            }
978
        }
979
        
980
        if(is_dir($filePath))
981
        {
982
            throw new FileHelper_Exception(
983
                sprintf('Cannot save file: the target [%s] is a directory.', basename($filePath)),
984
                sprintf(
985
                    'Tried saving content to path [%s].',
986
                    $filePath
987
                ),
988
                self::ERROR_CANNOT_WRITE_TO_FOLDER
989
            );
990
        }
991
        
992
        if(file_put_contents($filePath, $content) !== false) {
993
            return;
994
        }
995
        
996
        throw new FileHelper_Exception(
997
            sprintf('Cannot save file: writing content to the file [%s] failed.', basename($filePath)),
998
            sprintf(
999
                'Tried saving content to file in path [%s].',
1000
                $filePath
1001
            ),
1002
            self::ERROR_SAVE_FILE_WRITE_FAILED
1003
        );
1004
    }
1005
    
1006
   /**
1007
    * Checks whether it is possible to run PHP command 
1008
    * line commands.
1009
    * 
1010
    * @return boolean
1011
    */
1012
    public static function canMakePHPCalls() : bool
1013
    {
1014
        return self::cliCommandExists('php');
1015
    }
1016
    
1017
    /**
1018
     * Determines if a command exists on the current environment's command line interface.
1019
     *
1020
     * @param string $command The name of the command to check, e.g. "php"
1021
     * @return bool True if the command has been found, false otherwise.
1022
     * @throws FileHelper_Exception 
1023
     * 
1024
     * @todo Move this to a separate class.
1025
     */
1026
    public static  function cliCommandExists($command)
1027
    {
1028
        static $checked = array();
1029
        
1030
        if(isset($checked[$command])) {
1031
            return $checked[$command];
1032
        }
1033
        
1034
        // command to use to search for available commands
1035
        // on the target OS
1036
        $osCommands = array(
1037
            'windows' => 'where',
1038
            'linux' => 'which'
1039
        );
1040
        
1041
        $os = strtolower(PHP_OS_FAMILY);
1042
        
1043
        if(!isset($osCommands[$os])) 
1044
        {
1045
            throw new FileHelper_Exception(
1046
                'Unsupported OS for CLI commands',
1047
                sprintf(
1048
                    'The command to search for available CLI commands is not known for the OS [%s].',
1049
                    $os
1050
                ),
1051
                self::ERROR_UNSUPPORTED_OS_CLI_COMMAND
1052
            );
1053
        }
1054
        
1055
        $whereCommand = $osCommands[$os];
1056
        
1057
        $pipes = array();
1058
        
1059
        $process = proc_open(
1060
            $whereCommand.' '.$command,
1061
            array(
1062
                0 => array("pipe", "r"), //STDIN
1063
                1 => array("pipe", "w"), //STDOUT
1064
                2 => array("pipe", "w"), //STDERR
1065
            ),
1066
            $pipes
1067
        );
1068
        
1069
        if($process === false) {
1070
            $checked[$command] = false;
1071
            return false;
1072
        }
1073
        
1074
        $stdout = stream_get_contents($pipes[1]);
1075
        
1076
        fclose($pipes[1]);
1077
        fclose($pipes[2]);
1078
        
1079
        proc_close($process);
1080
        
1081
        $result = $stdout != '';
1082
        
1083
        $checked[$command] = $result;
1084
        
1085
        return $result;
1086
    }
1087
    
1088
   /**
1089
    * Validates a PHP file's syntax.
1090
    * 
1091
    * NOTE: This will fail silently if the PHP command line
1092
    * is not available. Use {@link FileHelper::canMakePHPCalls()}
1093
    * to check this beforehand as needed.
1094
    * 
1095
    * @param string $path
1096
    * @return boolean|array A boolean true if the file is valid, an array with validation messages otherwise.
1097
    */
1098
    public static function checkPHPFileSyntax($path)
1099
    {
1100
        if(!self::canMakePHPCalls()) {
1101
            return true;
1102
        }
1103
        
1104
        $output = array();
1105
        $command = sprintf('php -l "%s" 2>&1', $path);
1106
        exec($command, $output);
1107
        
1108
        // when the validation is successful, the first entry
1109
        // in the array contains the success message. When it
1110
        // is invalid, the first entry is always empty.
1111
        if(!empty($output[0])) {
1112
            return true;
1113
        }
1114
        
1115
        array_shift($output); // the first entry is always empty
1116
        array_pop($output); // the last message is a superfluous message saying there's an error
1117
        
1118
        return $output;
1119
    }
1120
    
1121
   /**
1122
    * Retrieves the last modified date for the specified file or folder.
1123
    * 
1124
    * Note: If the target does not exist, returns null. 
1125
    * 
1126
    * @param string $path
1127
    * @return \DateTime|NULL
1128
    */
1129
    public static function getModifiedDate($path)
1130
    {
1131
        $time = filemtime($path);
1132
        if($time !== false) {
1133
            $date = new \DateTime();
1134
            $date->setTimestamp($time);
1135
            return $date;
1136
        }
1137
        
1138
        return null; 
1139
    }
1140
    
1141
   /**
1142
    * Retrieves the names of all subfolders in the specified path.
1143
    * 
1144
    * Available options:
1145
    * 
1146
    * - recursive: true/false
1147
    *   Whether to search for subfolders recursively. 
1148
    *   
1149
    * - absolute-paths: true/false
1150
    *   Whether to return a list of absolute paths.
1151
    * 
1152
    * @param string $targetFolder
1153
    * @param array $options
1154
    * @throws FileHelper_Exception
1155
    * @return string[]
1156
    * 
1157
    * @todo Move this to a separate class.
1158
    */
1159
    public static function getSubfolders($targetFolder, $options = array())
1160
    {
1161
        if(!is_dir($targetFolder)) 
1162
        {
1163
            throw new FileHelper_Exception(
1164
                'Target folder does not exist',
1165
                sprintf(
1166
                    'Cannot retrieve subfolders from [%s], the folder does not exist.',
1167
                    $targetFolder
1168
                ),
1169
                self::ERROR_FIND_SUBFOLDERS_FOLDER_DOES_NOT_EXIST
1170
            );
1171
        }
1172
        
1173
        $options = array_merge(
1174
            array(
1175
                'recursive' => false,
1176
                'absolute-path' => false
1177
            ), 
1178
            $options
1179
        );
1180
        
1181
        $result = array();
1182
        
1183
        $d = new \DirectoryIterator($targetFolder);
1184
        
1185
        foreach($d as $item) 
1186
        {
1187
            if($item->isDir() && !$item->isDot()) 
1188
            {
1189
                $name = $item->getFilename();
1190
                
1191
                if(!$options['absolute-path']) {
1192
                    $result[] = $name;
1193
                } else {
1194
                    $result[] = $targetFolder.'/'.$name;
1195
                }
1196
                
1197
                if(!$options['recursive']) 
1198
                {
1199
                    continue;
1200
                }
1201
                
1202
                $subs = self::getSubfolders($targetFolder.'/'.$name, $options);
1203
                foreach($subs as $sub) 
1204
                {
1205
                    $relative = $name.'/'.$sub;
1206
                    
1207
                    if(!$options['absolute-path']) {
1208
                        $result[] = $relative;
1209
                    } else {
1210
                        $result[] = $targetFolder.'/'.$relative;
1211
                    }
1212
                }
1213
            }
1214
        }
1215
        
1216
        return $result;
1217
    }
1218
1219
   /**
1220
    * Retrieves the maximum allowed upload file size, in bytes.
1221
    * Takes into account the PHP ini settings <code>post_max_size</code>
1222
    * and <code>upload_max_filesize</code>. Since these cannot
1223
    * be modified at runtime, they are the hard limits for uploads.
1224
    * 
1225
    * NOTE: Based on binary values, where 1KB = 1024 Bytes.
1226
    * 
1227
    * @return int Will return <code>-1</code> if no limit.
1228
    */
1229
    public static function getMaxUploadFilesize() : int
1230
    {
1231
        static $max_size = -1;
1232
        
1233
        if ($max_size < 0)
1234
        {
1235
            // Start with post_max_size.
1236
            $post_max_size = self::parse_size(ini_get('post_max_size'));
1237
            if ($post_max_size > 0) {
1238
                $max_size = $post_max_size;
1239
            }
1240
            
1241
            // If upload_max_size is less, then reduce. Except if upload_max_size is
1242
            // zero, which indicates no limit.
1243
            $upload_max = self::parse_size(ini_get('upload_max_filesize'));
1244
            if ($upload_max > 0 && $upload_max < $max_size) {
1245
                $max_size = $upload_max;
1246
            }
1247
        }
1248
        
1249
        return $max_size;
1250
    }
1251
    
1252
    protected static function parse_size(string $size) : float
1253
    {
1254
        $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
1255
        $size = floatval(preg_replace('/[^0-9\.]/', '', $size)); // Remove the non-numeric characters from the size.
1256
        
1257
        if($unit) 
1258
        {
1259
            // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
1260
            return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
1261
        }
1262
        
1263
        return round($size);
1264
    }
1265
   
1266
   /**
1267
    * Makes a path relative using a folder depth: will reduce the
1268
    * length of the path so that only the amount of folders defined
1269
    * in the <code>$depth</code> attribute are shown below the actual
1270
    * folder or file in the path.
1271
    *  
1272
    * @param string  $path The absolute or relative path
1273
    * @param int $depth The folder depth to reduce the path to
1274
    * @return string
1275
    */
1276
    public static function relativizePathByDepth(string $path, int $depth=2) : string
1277
    {
1278
        $path = self::normalizePath($path);
1279
        
1280
        $tokens = explode('/', $path);
1281
        $tokens = array_filter($tokens); // remove empty entries (trailing slash for example)
1282
        $tokens = array_values($tokens); // re-index keys
1283
        
1284
        if(empty($tokens)) {
1285
            return '';
1286
        }
1287
        
1288
        // remove the drive if present
1289
        if(strstr($tokens[0], ':')) {
1290
            array_shift($tokens);
1291
        }
1292
        
1293
        // path was only the drive
1294
        if(count($tokens) == 0) {
1295
            return '';
1296
        }
1297
1298
        // the last element (file or folder)
1299
        $target = array_pop($tokens);
1300
        
1301
        // reduce the path to the specified depth
1302
        $length = count($tokens);
1303
        if($length > $depth) {
1304
            $tokens = array_slice($tokens, $length-$depth);
1305
        }
1306
1307
        // append the last element again
1308
        $tokens[] = $target;
1309
        
1310
        return trim(implode('/', $tokens), '/');
1311
    }
1312
    
1313
   /**
1314
    * Makes the specified path relative to another path,
1315
    * by removing one from the other if found. Also 
1316
    * normalizes the path to use forward slashes. 
1317
    * 
1318
    * Example:
1319
    * 
1320
    * <pre>
1321
    * relativizePath('c:\some\folder\to\file.txt', 'c:\some\folder');
1322
    * </pre>
1323
    * 
1324
    * Result: <code>to/file.txt</code>
1325
    * 
1326
    * @param string $path
1327
    * @param string $relativeTo
1328
    * @return string
1329
    */
1330
    public static function relativizePath(string $path, string $relativeTo) : string
1331
    {
1332
        $path = self::normalizePath($path);
1333
        $relativeTo = self::normalizePath($relativeTo);
1334
        
1335
        $relative = str_replace($relativeTo, '', $path);
1336
        $relative = trim($relative, '/');
1337
        
1338
        return $relative;
1339
    }
1340
    
1341
   /**
1342
    * Checks that the target file exists, and throws an exception
1343
    * if it does not. 
1344
    * 
1345
    * @param string $path
1346
    * @param int|NULL $errorCode Optional custom error code
1347
    * @throws FileHelper_Exception
1348
    * @return string The real path to the file
1349
    * 
1350
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1351
    */
1352
    public static function requireFileExists(string $path, $errorCode=null) : string
1353
    {
1354
        $result = realpath($path);
1355
        if($result !== false) {
1356
            return $result;
1357
        }
1358
        
1359
        if($errorCode === null) {
1360
            $errorCode = self::ERROR_FILE_DOES_NOT_EXIST;
1361
        }
1362
        
1363
        throw new FileHelper_Exception(
1364
            sprintf('File [%s] does not exist.', basename($path)),
1365
            sprintf('Tried finding the file in path [%s].', $path),
1366
            $errorCode
1367
        );
1368
    }
1369
    
1370
   /**
1371
    * Reads a specific line number from the target file and returns its
1372
    * contents, if the file has such a line. Does so with little memory
1373
    * usage, as the file is not read entirely into memory.
1374
    * 
1375
    * @param string $path
1376
    * @param int $lineNumber Note: 1-based; the first line is number 1.
1377
    * @return string|NULL Will return null if the requested line does not exist.
1378
    * @throws FileHelper_Exception
1379
    * 
1380
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1381
    */
1382
    public static function getLineFromFile(string $path, int $lineNumber) : ?string
1383
    {
1384
        self::requireFileExists($path);
1385
        
1386
        $file = new \SplFileObject($path);
1387
        
1388
        if($file->eof()) {
1389
            return '';
1390
        }
1391
        
1392
        $targetLine = $lineNumber-1;
1393
        
1394
        $file->seek($targetLine);
1395
        
1396
        if($file->key() !== $targetLine) {
1397
             return null;
1398
        }
1399
        
1400
        return $file->current(); 
1401
    }
1402
    
1403
   /**
1404
    * Retrieves the total amount of lines in the file, without 
1405
    * reading the whole file into memory.
1406
    * 
1407
    * @param string $path
1408
    * @return int
1409
    */
1410
    public static function countFileLines(string $path) : int
1411
    {
1412
        self::requireFileExists($path);
1413
        
1414
        $spl = new \SplFileObject($path);
1415
        
1416
        // tries seeking as far as possible
1417
        $spl->seek(PHP_INT_MAX);
1418
        
1419
        $number = $spl->key();
1420
        
1421
        // if seeking to the end the cursor is still at 0, there are no lines. 
1422
        if($number === 0) 
1423
        {
1424
            // since it's a very small file, to get reliable results,
1425
            // we read its contents and use that to determine what
1426
            // kind of contents we are dealing with. Tests have shown 
1427
            // that this is not pactical to solve with the SplFileObject.
1428
            $content = file_get_contents($path);
1429
            
1430
            if(empty($content)) {
1431
                return 0;
1432
            }
1433
        }
1434
        
1435
        // return the line number we were able to reach + 1 (key is zero-based)
1436
        return $number+1;
1437
    }
1438
    
1439
   /**
1440
    * Parses the target file to detect any PHP classes contained
1441
    * within, and retrieve information on them. Does not use the 
1442
    * PHP reflection API.
1443
    * 
1444
    * @param string $filePath
1445
    * @return FileHelper_PHPClassInfo
1446
    */
1447
    public static function findPHPClasses(string $filePath) : FileHelper_PHPClassInfo
1448
    {
1449
        return new FileHelper_PHPClassInfo($filePath);
1450
    }
1451
    
1452
   /**
1453
    * Detects the end of line style used in the target file, if any.
1454
    * Can be used with large files, because it only reads part of it.
1455
    * 
1456
    * @param string $filePath The path to the file.
1457
    * @return NULL|ConvertHelper_EOL The end of line character information, or NULL if none is found.
1458
    */
1459
    public static function detectEOLCharacter(string $filePath) : ?ConvertHelper_EOL
1460
    {
1461
        // 20 lines is enough to get a good picture of the newline style in the file.
1462
        $amount = 20;
1463
        
1464
        $lines = self::readLines($filePath, $amount);
1465
        
1466
        $string = implode('', $lines);
1467
        
1468
        return ConvertHelper::detectEOLCharacter($string);
1469
    }
1470
    
1471
   /**
1472
    * Reads the specified amount of lines from the target file.
1473
    * Unicode BOM compatible: any byte order marker is stripped
1474
    * from the resulting lines.
1475
    * 
1476
    * @param string $filePath
1477
    * @param int $amount Set to 0 to read all lines.
1478
    * @return array
1479
    * 
1480
    * @see FileHelper::ERROR_CANNOT_OPEN_FILE_TO_READ_LINES
1481
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1482
    */
1483
    public static function readLines(string $filePath, int $amount=0) : array
1484
    {
1485
        self::requireFileExists($filePath);
1486
        
1487
        $fn = fopen($filePath, "r");
1488
        
1489
        if($fn === false) 
1490
        {
1491
            throw new FileHelper_Exception(
1492
                'Could not open file for reading.',
1493
                sprintf(
1494
                    'Tried accessing file at [%s].',
1495
                    $filePath
1496
                ),
1497
                self::ERROR_CANNOT_OPEN_FILE_TO_READ_LINES
1498
            );
1499
        }
1500
        
1501
        $result = array();
1502
        $counter = 0;
1503
        $first = true;
1504
        
1505
        while(!feof($fn)) 
1506
        {
1507
            $counter++;
1508
            
1509
            $line = fgets($fn);
1510
            
1511
            // can happen with zero length files
1512
            if($line === false) {
1513
                continue;
1514
            }
1515
            
1516
            // the first line may contain a unicode BOM marker.
1517
            if($first) 
1518
            {
1519
                $line = ConvertHelper::stripUTFBom($line);
1520
                $first = false;
1521
            }
1522
            
1523
            $result[] = $line;
1524
            
1525
            if($amount > 0 && $counter == $amount) {
1526
                break;
1527
            }
1528
        }
1529
        
1530
        fclose($fn);
1531
        
1532
        return $result;
1533
    }
1534
    
1535
   /**
1536
    * Reads all content from a file.
1537
    * 
1538
    * @param string $filePath
1539
    * @throws FileHelper_Exception
1540
    * @return string
1541
    * 
1542
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1543
    * @see FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
1544
    */
1545
    public static function readContents(string $filePath) : string
1546
    {
1547
        self::requireFileExists($filePath);
1548
        
1549
        $result = file_get_contents($filePath);
1550
        
1551
        if($result !== false) {
1552
            return $result;
1553
        }
1554
        
1555
        throw new FileHelper_Exception(
1556
            sprintf('Cannot read contents of file [%s].', basename($filePath)),
1557
            sprintf(
1558
                'Tried opening file for reading at: [%s].',
1559
                $filePath
1560
            ),
1561
            self::ERROR_CANNOT_READ_FILE_CONTENTS
1562
        );
1563
    }
1564
1565
   /**
1566
    * Ensures that the target path exists on disk, and is a folder.
1567
    * 
1568
    * @param string $path
1569
    * @return string The real path, with normalized slashes.
1570
    * @throws FileHelper_Exception
1571
    * 
1572
    * @see FileHelper::normalizePath()
1573
    * 
1574
    * @see FileHelper::ERROR_FOLDER_DOES_NOT_EXIST
1575
    * @see FileHelper::ERROR_PATH_IS_NOT_A_FOLDER
1576
    */
1577
    public static function requireFolderExists(string $path) : string
1578
    {
1579
        $actual = realpath($path);
1580
        
1581
        if($actual === false) 
1582
        {
1583
            throw new FileHelper_Exception(
1584
                'Folder does not exist',
1585
                sprintf(
1586
                    'The path [%s] does not exist on disk.',
1587
                    $path
1588
                ),
1589
                self::ERROR_FOLDER_DOES_NOT_EXIST
1590
            );
1591
        }
1592
        
1593
        if(is_dir($path)) 
1594
        {
1595
            return self::normalizePath($actual);
1596
        }
1597
        
1598
        throw new FileHelper_Exception(
1599
            'Target is not a folder',
1600
            sprintf(
1601
                'The path [%s] does not point to a folder.',
1602
                $path
1603
            ),
1604
            self::ERROR_PATH_IS_NOT_A_FOLDER
1605
        );
1606
    }
1607
}
1608