Passed
Push — master ( 9adcd8...42367a )
by Sebastian
02:21
created

FileHelper::getExtension()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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