Passed
Push — master ( 43d553...f25e71 )
by Sebastian
02:52
created

FileHelper   F

Complexity

Total Complexity 145

Size/Duplication

Total Lines 1588
Duplicated Lines 0 %

Importance

Changes 21
Bugs 1 Features 0
Metric Value
wmc 145
eloc 542
c 21
b 1
f 0
dl 0
loc 1588
rs 2

47 Methods

Rating   Name   Duplication   Size   Complexity  
A createFolder() 0 10 3
A deleteFile() 0 17 3
A parseCSVString() 0 12 2
A copyFile() 0 47 5
A createCSVParser() 0 12 3
A copyTree() 0 29 6
A parseSerializedFile() 0 31 3
B deleteTree() 0 32 9
A openUnserialized() 0 3 1
A readContents() 0 17 2
A saveFile() 0 49 5
A parseJSONFile() 0 33 5
A getKnownUnicodeEncodings() 0 3 1
B readLines() 0 50 7
A normalizePath() 0 3 1
A getModifiedDate() 0 10 2
A sendFile() 0 36 4
A getExtension() 0 14 3
A isPHPFile() 0 7 2
A fixFileName() 0 27 3
A getLineFromFile() 0 19 3
A downloadFile() 0 49 4
A isValidUnicodeEncoding() 0 16 2
A saveAsJSON() 0 21 3
A detectMimeType() 0 8 2
B getSubfolders() 0 58 9
A canMakePHPCalls() 0 3 1
A requireFolderExists() 0 28 3
A requireFileExists() 0 15 3
A getMaxUploadFilesize() 0 21 5
A findPHPFiles() 0 3 1
A findHTMLFiles() 0 3 1
A relativizePathByDepth() 0 35 5
A checkPHPFileSyntax() 0 21 3
A createFileFinder() 0 3 1
A countFileLines() 0 27 3
A findPHPClasses() 0 3 1
A getFilename() 0 14 3
A detectUTFBom() 0 27 4
A detectEOLCharacter() 0 10 1
A cliCommandExists() 0 60 4
A parseCSVFile() 0 5 1
A getUTFBOMs() 0 13 2
A relativizePath() 0 9 1
A findFiles() 0 23 6
A parse_size() 0 12 2
A removeExtension() 0 6 1

How to fix   Complexity   

Complex Class

Complex classes like FileHelper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FileHelper, and based on these observations, apply Extract Interface, too.

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