Passed
Push — master ( 1f0193...943848 )
by Sebastian
03:12
created

FileHelper::checkPHPFileSyntax()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 21
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 21
rs 9.9332
cc 3
nc 3
nop 1
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
use DateTime;
13
use DirectoryIterator;
14
use ParseCsv\Csv;
15
16
/**
17
 * Collection of file system related methods.
18
 * 
19
 * @package Application Utils
20
 * @subpackage FileHelper
21
 * @author Sebastian Mordziol <[email protected]>
22
 */
23
class FileHelper
24
{
25
    const ERROR_CANNOT_FIND_JSON_FILE = 340001;
26
    const ERROR_JSON_FILE_CANNOT_BE_READ = 340002;
27
    const ERROR_CANNOT_DECODE_JSON_FILE = 340003;
28
    const ERROR_CANNOT_SEND_MISSING_FILE = 340004;
29
    const ERROR_JSON_ENCODE_ERROR = 340005;
30
    const ERROR_CANNOT_OPEN_URL = 340008;
31
    const ERROR_CANNOT_CREATE_FOLDER = 340009;
32
    const ERROR_FILE_NOT_READABLE = 340010;
33
    const ERROR_CANNOT_COPY_FILE = 340011;
34
    const ERROR_CANNOT_DELETE_FILE = 340012;
35
    const ERROR_FIND_SUBFOLDERS_FOLDER_DOES_NOT_EXIST = 340014;
36
    const ERROR_UNKNOWN_FILE_MIME_TYPE = 340015;
37
    const ERROR_SERIALIZED_FILE_CANNOT_BE_READ = 340017;
38
    const ERROR_SERIALIZED_FILE_UNSERIALZE_FAILED = 340018;
39
    const ERROR_UNSUPPORTED_OS_CLI_COMMAND = 340019;
40
    const ERROR_SOURCE_FILE_NOT_FOUND = 340020;
41
    const ERROR_SOURCE_FILE_NOT_READABLE = 340021;
42
    const ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE = 340022;
43
    const ERROR_SAVE_FOLDER_NOT_WRITABLE = 340023;
44
    const ERROR_SAVE_FILE_NOT_WRITABLE = 340024;
45
    const ERROR_SAVE_FILE_WRITE_FAILED = 340025;
46
    const ERROR_FILE_DOES_NOT_EXIST = 340026;
47
    const ERROR_CANNOT_OPEN_FILE_TO_READ_LINES = 340027;
48
    const ERROR_CANNOT_READ_FILE_CONTENTS = 340028;
49
    const ERROR_PARSING_CSV = 340029;
50
    const ERROR_CURL_INIT_FAILED = 340030;
51
    const ERROR_CURL_OUTPUT_NOT_STRING = 340031;
52
    const ERROR_CANNOT_OPEN_FILE_TO_DETECT_BOM = 340032;
53
    const ERROR_FOLDER_DOES_NOT_EXIST = 340033;
54
    const ERROR_PATH_IS_NOT_A_FOLDER = 340034;
55
    const ERROR_CANNOT_WRITE_TO_FOLDER = 340035;
56
57
    /**
58
     * @var array<string,string>|NULL
59
     */
60
    protected static $utfBoms;
61
62
    /**
63
    * Opens a serialized file and returns the unserialized data.
64
    * 
65
    * @param string $file
66
    * @throws FileHelper_Exception
67
    * @return array
68
    * @deprecated Use parseSerializedFile() instead.
69
    * @see FileHelper::parseSerializedFile()
70
    */
71
    public static function openUnserialized(string $file) : array
72
    {
73
        return self::parseSerializedFile($file);
74
    }
75
76
   /**
77
    * Opens a serialized file and returns the unserialized data.
78
    *
79
    * @param string $file
80
    * @throws FileHelper_Exception
81
    * @return array
82
    * @see FileHelper::parseSerializedFile()
83
    * 
84
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
85
    * @see FileHelper::ERROR_SERIALIZED_FILE_CANNOT_BE_READ
86
    * @see FileHelper::ERROR_SERIALIZED_FILE_UNSERIALZE_FAILED
87
    */
88
    public static function parseSerializedFile(string $file) : array
89
    {
90
        self::requireFileExists($file);
91
        
92
        $contents = file_get_contents($file);
93
        
94
        if($contents === false) 
95
        {
96
            throw new FileHelper_Exception(
97
                'Cannot load serialized content from file.',
98
                sprintf(
99
                    'Tried reading file contents at [%s].',
100
                    $file
101
                ),
102
                self::ERROR_SERIALIZED_FILE_CANNOT_BE_READ
103
            );
104
        }
105
        
106
        $result = @unserialize($contents);
107
        
108
        if($result !== false) {
109
            return $result;
110
        }
111
        
112
        throw new FileHelper_Exception(
113
            'Cannot unserialize the file contents.',
114
            sprintf(
115
                'Tried unserializing the data from file at [%s].',
116
                $file
117
            ),
118
            self::ERROR_SERIALIZED_FILE_UNSERIALZE_FAILED
119
        );
120
    }
121
122
    /**
123
     * Deletes a folder tree with all files therein, including
124
     * the specified folder itself.
125
     *
126
     * @param string $rootFolder
127
     * @return bool
128
     */
129
    public static function deleteTree(string $rootFolder) : bool
130
    {
131
        if(!file_exists($rootFolder)) {
132
            return true;
133
        }
134
        
135
        $d = new DirectoryIterator($rootFolder);
136
        foreach ($d as $item) {
137
            if ($item->isDot()) {
138
                continue;
139
            }
140
141
            $itemPath = $item->getRealPath();
142
            if (!is_readable($itemPath)) {
143
                return false;
144
            }
145
146
            if ($item->isDir()) {
147
                if (!FileHelper::deleteTree($itemPath)) {
148
                    return false;
149
                }
150
                continue;
151
            }
152
153
            if ($item->isFile()) {
154
                if (!unlink($itemPath)) {
155
                    return false;
156
                }
157
            }
158
        }
159
160
        return rmdir($rootFolder);
161
    }
162
    
163
   /**
164
    * Create a folder, if it does not exist yet.
165
    *  
166
    * @param string $path
167
    * @throws FileHelper_Exception
168
    * @see FileHelper::ERROR_CANNOT_CREATE_FOLDER
169
    */
170
    public static function createFolder(string $path) : void
171
    {
172
        if(is_dir($path) || mkdir($path, 0777, true)) {
173
            return;
174
        }
175
        
176
        throw new FileHelper_Exception(
177
            sprintf('Could not create target folder [%s].', basename($path)),
178
            sprintf('Tried to create the folder in path [%s].', $path),
179
            self::ERROR_CANNOT_CREATE_FOLDER
180
        );
181
    }
182
183
    /**
184
     * Copies a folder tree to the target folder.
185
     *
186
     * @param string $source
187
     * @param string $target
188
     * @throws FileHelper_Exception
189
     */
190
    public static function copyTree(string $source, string $target) : void
191
    {
192
        self::createFolder($target);
193
194
        $d = new DirectoryIterator($source);
195
        foreach ($d as $item) 
196
        {
197
            if ($item->isDot()) {
198
                continue;
199
            }
200
201
            $itemPath = self::requireFileReadable($item->getPathname());
202
            
203
            $baseName = basename($itemPath);
204
205
            if ($item->isDir()) 
206
            {
207
                FileHelper::copyTree($itemPath, $target . '/' . $baseName);
208
            } 
209
            else if($item->isFile()) 
210
            {
211
                self::copyFile($itemPath, $target . '/' . $baseName);
212
            }
213
        }
214
    }
215
    
216
   /**
217
    * Copies a file to the target location. Includes checks
218
    * for most error sources, like the source file not being
219
    * readable. Automatically creates the target folder if it
220
    * does not exist yet.
221
    * 
222
    * @param string $sourcePath
223
    * @param string $targetPath
224
    * @throws FileHelper_Exception
225
    * 
226
    * @see FileHelper::ERROR_CANNOT_CREATE_FOLDER
227
    * @see FileHelper::ERROR_SOURCE_FILE_NOT_FOUND
228
    * @see FileHelper::ERROR_SOURCE_FILE_NOT_READABLE
229
    * @see FileHelper::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE
230
    * @see FileHelper::ERROR_CANNOT_COPY_FILE
231
    */
232
    public static function copyFile(string $sourcePath, string $targetPath) : void
233
    {
234
        self::requireFileExists($sourcePath, self::ERROR_SOURCE_FILE_NOT_FOUND);
235
        self::requireFileReadable($sourcePath, self::ERROR_SOURCE_FILE_NOT_READABLE);
236
        
237
        $targetFolder = dirname($targetPath);
238
        
239
        if(!file_exists($targetFolder))
240
        {
241
            self::createFolder($targetFolder);
242
        }
243
        else if(!is_writable($targetFolder)) 
244
        {
245
            throw new FileHelper_Exception(
246
                sprintf('Target folder [%s] is not writable.', basename($targetFolder)),
247
                sprintf(
248
                    'Tried copying to target folder [%s].',
249
                    $targetFolder
250
                ),
251
                self::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE
252
            );
253
        }
254
        
255
        if(copy($sourcePath, $targetPath)) {
256
            return;
257
        }
258
        
259
        throw new FileHelper_Exception(
260
            sprintf('Cannot copy file [%s].', basename($sourcePath)),
261
            sprintf(
262
                'The file [%s] could not be copied from [%s] to [%s].',
263
                basename($sourcePath),
264
                $sourcePath,
265
                $targetPath
266
            ),
267
            self::ERROR_CANNOT_COPY_FILE
268
        );
269
    }
270
    
271
   /**
272
    * Deletes the target file. Ignored if it cannot be found,
273
    * and throws an exception if it fails.
274
    * 
275
    * @param string $filePath
276
    * @throws FileHelper_Exception
277
    * 
278
    * @see FileHelper::ERROR_CANNOT_DELETE_FILE
279
    */
280
    public static function deleteFile(string $filePath) : void
281
    {
282
        if(!file_exists($filePath)) {
283
            return;
284
        }
285
        
286
        if(unlink($filePath)) {
287
            return;
288
        }
289
        
290
        throw new FileHelper_Exception(
291
            sprintf('Cannot delete file [%s].', basename($filePath)),
292
            sprintf(
293
                'The file [%s] cannot be deleted.',
294
                $filePath
295
            ),
296
            self::ERROR_CANNOT_DELETE_FILE
297
        );
298
    }
299
300
    /**
301
    * Creates a new CSV parser instance and returns it.
302
    * 
303
    * @param string $delimiter
304
    * @param string $enclosure
305
    * @param string $escape
306
    * @param bool $heading
307
    * @return Csv
308
     * @see CSVHelper::createParser()
309
    */
310
    public static function createCSVParser(string $delimiter = ';', string $enclosure = '"', string $escape = '\\', bool $heading=false) : Csv
311
    {
312
        if($delimiter==='') { $delimiter = ';'; }
313
        if($enclosure==='') { $enclosure = '"'; }
314
315
        $parser = CSVHelper::createParser($delimiter);
316
        $parser->enclosure = $enclosure;
317
        $parser->heading = $heading;
318
319
        return $parser;
320
    }
321
322
   /**
323
    * Parses all lines in the specified string and returns an
324
    * indexed array with all csv values in each line.
325
    *
326
    * @param string $csv
327
    * @param string $delimiter
328
    * @param string $enclosure
329
    * @param string $escape
330
    * @param bool $heading
331
    * @return array
332
    * @throws FileHelper_Exception
333
    * 
334
    * @see parseCSVFile()
335
    * @see FileHelper::ERROR_PARSING_CSV
336
    */
337
    public static function parseCSVString(string $csv, string $delimiter = ';', string $enclosure = '"', string $escape = '\\', bool $heading=false) : array
338
    {
339
        $parser = self::createCSVParser($delimiter, $enclosure, '\\', $heading);
340
341
        if($parser->parse($csv))
342
        {
343
            return $parser->data;
344
        }
345
346
        throw new FileHelper_Exception(
347
            'Could not parse CSV string, possible formatting error.',
348
            'The parseCSV library returned an error, but exact details are not available.',
349
            self::ERROR_PARSING_CSV
350
        );
351
    }
352
353
    /**
354
     * Parses all lines in the specified file and returns an
355
     * indexed array with all csv values in each line.
356
     *
357
     * @param string $filePath
358
     * @param string $delimiter 
359
     * @param string $enclosure The character to use to quote literal strings
360
     * @param string $escape The character to use to escape special characters.
361
     * @param bool $heading Whether to include headings.
362
     * @return array
363
     * @throws FileHelper_Exception
364
     * 
365
     * @see parseCSVString()
366
     * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
367
     * @see FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
368
     */
369
    public static function parseCSVFile(string $filePath, string $delimiter = ';', string $enclosure = '"', string $escape = '\\', bool $heading=false) : array
370
    {
371
        $content = self::readContents($filePath);
372
373
        return self::parseCSVString($content, $delimiter, $enclosure, $escape, $heading);
374
    }
375
376
    /**
377
     * Detects the mime type for the specified file name/path.
378
     * Returns null if it is not a known file extension.
379
     *
380
     * @param string $fileName
381
     * @return string|NULL
382
     */
383
    public static function detectMimeType(string $fileName) : ?string
384
    {
385
        $ext = self::getExtension($fileName);
386
        if(empty($ext)) {
387
            return null;
388
        }
389
390
        return FileHelper_MimeTypes::getMime($ext);
391
    }
392
393
    /**
394
     * Like `sendFile()`, but automatically determines whether
395
     * the browser can open the target file type, to either
396
     * send it directly to the browser, or force downloading
397
     * it instead.
398
     *
399
     * @param string $filePath
400
     * @param string $fileName
401
     * @throws FileHelper_Exception
402
     */
403
    public function sendFileAuto(string $filePath, string $fileName = '') : void
404
    {
405
        self::sendFile(
406
            $filePath,
407
            $fileName,
408
            !FileHelper_MimeTypes::canBrowserDisplay(self::getExtension($filePath))
409
        );
410
    }
411
412
    /**
413
     * Detects the mime type of the target file automatically,
414
     * sends the required headers to trigger a download and
415
     * outputs the file. Returns false if the mime type could
416
     * not be determined.
417
     * 
418
     * @param string $filePath
419
     * @param string|null $fileName The name of the file for the client.
420
     * @param bool $asAttachment Whether to force the client to download the file.
421
     * @throws FileHelper_Exception
422
     * 
423
     * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
424
     * @see FileHelper::ERROR_UNKNOWN_FILE_MIME_TYPE
425
     */
426
    public static function sendFile(string $filePath, $fileName = null, bool $asAttachment=true)
427
    {
428
        self::requireFileExists($filePath);
429
        
430
        if(empty($fileName)) {
431
            $fileName = basename($filePath);
432
        }
433
434
        $mime = self::detectMimeType($filePath);
435
        if (!$mime) {
436
            throw new FileHelper_Exception(
437
                'Unknown file mime type',
438
                sprintf(
439
                    'Could not determine mime type for file name [%s].',
440
                    basename($filePath)
441
                ),
442
                self::ERROR_UNKNOWN_FILE_MIME_TYPE
443
            );
444
        }
445
        
446
        header("Cache-Control: public", true);
447
        header("Content-Description: File Transfer", true);
448
        header("Content-Type: " . $mime, true);
449
450
        $disposition = 'inline';
451
        if($asAttachment) {
452
            $disposition = 'attachment';
453
        }
454
        
455
        header(sprintf(
456
            "Content-Disposition: %s; filename=%s",
457
            $disposition,
458
            '"'.$fileName.'"'
459
        ), true);
460
        
461
        readfile($filePath);
462
    }
463
464
    /**
465
     * Uses cURL to download the contents of the specified URL,
466
     * returns the content.
467
     *
468
     * @param string $url
469
     * @throws FileHelper_Exception
470
     * @return string
471
     * 
472
     * @see FileHelper::ERROR_CANNOT_OPEN_URL
473
     */
474
    public static function downloadFile(string $url) : string
475
    {
476
        $ch = curl_init();
477
        if(!is_resource($ch)) 
478
        {
479
            throw new FileHelper_Exception(
480
                'Could not initialize a new cURL instance.',
481
                'Calling curl_init returned false. Additional information is not available.',
482
                self::ERROR_CURL_INIT_FAILED
483
            );
484
        }
485
        
486
        curl_setopt($ch, CURLOPT_URL, $url);
487
        curl_setopt($ch, CURLOPT_REFERER, $url);
488
        curl_setopt($ch, CURLOPT_USERAGENT, "Google Chrome/1.0");
489
        curl_setopt($ch, CURLOPT_HEADER, 0);
490
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
491
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
492
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
493
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
494
        curl_setopt($ch, CURLOPT_TIMEOUT, 100000);
495
        
496
        $output = curl_exec($ch);
497
498
        if($output === false) {
499
            throw new FileHelper_Exception(
500
                'Unable to open URL',
501
                sprintf(
502
                    'Tried accessing URL "%1$s" using cURL, but the request failed. cURL error: %2$s',
503
                    $url,
504
                    curl_error($ch)
505
                ),
506
                self::ERROR_CANNOT_OPEN_URL
507
            );
508
        }
509
510
        curl_close($ch);
511
512
        if(is_string($output)) 
513
        {
514
            return $output;
515
        }
516
        
517
        throw new FileHelper_Exception(
518
            'Unexpected cURL output.',
519
            'The cURL output is not a string, although the CURLOPT_RETURNTRANSFER option is set.',
520
            self::ERROR_CURL_OUTPUT_NOT_STRING
521
        );
522
    }
523
    
524
   /**
525
    * Verifies whether the target file is a PHP file. The path
526
    * to the file can be a path to a file as a string, or a 
527
    * DirectoryIterator object instance.
528
    * 
529
    * @param string|DirectoryIterator $pathOrDirIterator
530
    * @return boolean
531
    */
532
    public static function isPHPFile($pathOrDirIterator) : bool
533
    {
534
    	return self::getExtension($pathOrDirIterator) === 'php';
535
    }
536
    
537
   /**
538
    * Retrieves the extension of the specified file. Can be a path
539
    * to a file as a string, or a DirectoryIterator object instance.
540
    * 
541
    * @param string|DirectoryIterator $pathOrDirIterator
542
    * @param bool $lowercase
543
    * @return string
544
    */
545
    public static function getExtension($pathOrDirIterator, bool $lowercase = true) : string
546
    {
547
        if($pathOrDirIterator instanceof DirectoryIterator) {
548
            $filename = $pathOrDirIterator->getFilename();
549
        } else {
550
            $filename = basename(strval($pathOrDirIterator));
551
        }
552
         
553
        $ext = pathinfo($filename, PATHINFO_EXTENSION);
554
        if($lowercase) {
555
        	$ext = mb_strtolower($ext);
0 ignored issues
show
Bug introduced by
It seems like $ext can also be of type array; however, parameter $string of mb_strtolower() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

555
        	$ext = mb_strtolower(/** @scrutinizer ignore-type */ $ext);
Loading history...
556
        }
557
        
558
        return $ext;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $ext could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
559
    }
560
    
561
   /**
562
    * Retrieves the file name from a path, with or without extension.
563
    * The path to the file can be a string, or a DirectoryIterator object
564
    * instance.
565
    * 
566
    * In case of folders, behaves like the pathinfo function: returns
567
    * the name of the folder.
568
    * 
569
    * @param string|DirectoryIterator $pathOrDirIterator
570
    * @param bool $extension
571
    * @return string
572
    */
573
    public static function getFilename($pathOrDirIterator, $extension = true) : string
574
    {
575
        $path = strval($pathOrDirIterator);
576
    	if($pathOrDirIterator instanceof DirectoryIterator) {
577
    		$path = $pathOrDirIterator->getFilename();
578
    	}
579
    	
580
    	$path = self::normalizePath($path);
581
    	
582
    	if(!$extension) {
583
    	    return pathinfo($path, PATHINFO_FILENAME);
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($path, AppUtils\PATHINFO_FILENAME) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
584
    	}
585
    	
586
    	return pathinfo($path, PATHINFO_BASENAME); 
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($path, AppUtils\PATHINFO_BASENAME) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
587
    }
588
589
    /**
590
     * Tries to read the contents of the target file and
591
     * treat it as JSON to return the decoded JSON data.
592
     *
593
     * @param string $file
594
     * @param string $targetEncoding
595
     * @param string|string[]|null $sourceEncoding
596
     * @return array
597
     *
598
     * @throws FileHelper_Exception
599
     * @see FileHelper::ERROR_CANNOT_FIND_JSON_FILE
600
     * @see FileHelper::ERROR_CANNOT_DECODE_JSON_FILE
601
     */
602
    public static function parseJSONFile(string $file, string $targetEncoding='', $sourceEncoding=null) : array
603
    {
604
        self::requireFileExists($file, self::ERROR_CANNOT_FIND_JSON_FILE);
605
        
606
        $content = self::readContents($file);
607
608
        if(!empty($targetEncoding)) {
609
            $content = mb_convert_encoding($content, $targetEncoding, $sourceEncoding);
610
        }
611
        
612
        $json = json_decode($content, true);
0 ignored issues
show
Bug introduced by
It seems like $content can also be of type array; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

612
        $json = json_decode(/** @scrutinizer ignore-type */ $content, true);
Loading history...
613
        if($json === false || $json === NULL) {
614
            throw new FileHelper_Exception(
615
                'Cannot decode json data',
616
                sprintf(
617
                    'Loaded the contents of file [%s] successfully, but decoding it as JSON failed.',
618
                    $file    
619
                ),
620
                self::ERROR_CANNOT_DECODE_JSON_FILE
621
            );
622
        }
623
        
624
        return $json;
625
    }
626
    
627
   /**
628
    * Corrects common formatting mistakes when users enter
629
    * file names, like too many spaces, dots and the like.
630
    * 
631
    * NOTE: if the file name contains a path, the path is
632
    * stripped, leaving only the file name.
633
    * 
634
    * @param string $name
635
    * @return string
636
    */
637
    public static function fixFileName(string $name) : string
638
    {
639
        $name = trim($name);
640
        $name = self::normalizePath($name);
641
        $name = basename($name);
642
        
643
        $replaces = array(
644
            "\t" => ' ',
645
            "\r" => ' ',
646
            "\n" => ' ',
647
            ' .' => '.',
648
            '. ' => '.',
649
        );
650
        
651
        $name = str_replace(array_keys($replaces), array_values($replaces), $name);
652
        
653
        while(strstr($name, '  ')) {
654
            $name = str_replace('  ', ' ', $name);
655
        }
656
657
        $name = str_replace(array_keys($replaces), array_values($replaces), $name);
658
        
659
        while(strstr($name, '..')) {
660
            $name = str_replace('..', '.', $name);
661
        }
662
        
663
        return $name;
664
    }
665
666
    /**
667
     * Creates an instance of the file finder, which is an easier
668
     * alternative to the other manual findFile methods, since all
669
     * options can be set by chaining.
670
     *
671
     * @param string $path
672
     * @return FileHelper_FileFinder
673
     * @throws FileHelper_Exception
674
     *
675
     * @see FileHelper_FileFinder::ERROR_PATH_DOES_NOT_EXIST
676
     */
677
    public static function createFileFinder(string $path) : FileHelper_FileFinder
678
    {
679
        return new FileHelper_FileFinder($path);
680
    }
681
682
    /**
683
     * Searches for all HTML files in the target folder.
684
     *
685
     * NOTE: This method only exists for backwards compatibility.
686
     * Use the `createFileFinder()` method instead, which offers
687
     * an object oriented interface that is much easier to use.
688
     *
689
     * @param string $targetFolder
690
     * @param array $options
691
     * @return array An indexed array with files.
692
     * @throws FileHelper_Exception
693
     * @see FileHelper::createFileFinder()
694
     */
695
    public static function findHTMLFiles(string $targetFolder, array $options=array()) : array
696
    {
697
        return self::findFiles($targetFolder, array('html'), $options);
698
    }
699
700
    /**
701
     * Searches for all PHP files in the target folder.
702
     *
703
     * NOTE: This method only exists for backwards compatibility.
704
     * Use the `createFileFinder()` method instead, which offers
705
     * an object oriented interface that is much easier to use.
706
     *
707
     * @param string $targetFolder
708
     * @param array $options
709
     * @return string[] An indexed array of PHP files.
710
     * @throws FileHelper_Exception
711
     * @see FileHelper::createFileFinder()
712
     */
713
    public static function findPHPFiles(string $targetFolder, array $options=array()) : array
714
    {
715
        return self::findFiles($targetFolder, array('php'), $options);
716
    }
717
    
718
   /**
719
    * Finds files according to the specified options.
720
    * 
721
    * NOTE: This method only exists for backwards compatibility.
722
    * Use the `createFileFinder()` method instead, which offers
723
    * an object oriented interface that is much easier to use.
724
    *  
725
    * @param string $targetFolder
726
    * @param string[] $extensions
727
    * @param array<string,mixed> $options
728
    * @throws FileHelper_Exception
729
    * @return string[]
730
    * @see FileHelper::createFileFinder()
731
    */
732
    public static function findFiles(string $targetFolder, array $extensions=array(), array $options=array()) : array
733
    {
734
        $finder = self::createFileFinder($targetFolder);
735
736
        foreach ($extensions as $extension) {
737
            $finder->includeExtension($extension);
738
        }
739
740
        $finder->setPathmodeStrip();
741
        
742
        if(isset($options['relative-path']) && $options['relative-path'] === true) 
743
        {
744
            $finder->setPathmodeRelative();
745
        } 
746
        else if(isset($options['absolute-path']) && $options['absolute-path'] === true)
747
        {
748
            $finder->setPathmodeAbsolute();
749
        }
750
        
751
        if(isset($options['strip-extension'])) 
752
        {
753
            $finder->stripExtensions();
754
        }
755
        
756
        $finder->setOptions($options);
757
        
758
        return $finder->getAll();
759
    }
760
761
   /**
762
    * Removes the extension from the specified path or file name,
763
    * if any, and returns the name without the extension.
764
    * 
765
    * @param string $filename
766
    * @param bool $keepPath Whether to keep the path component, if any. Default PHP pathinfo behavior is not to.
767
    * @return string
768
    */
769
    public static function removeExtension(string $filename, bool $keepPath=false) : string
770
    {
771
        // normalize paths to allow windows style slashes even on nix servers
772
        $filename = self::normalizePath($filename);
773
        
774
        if(!$keepPath) 
775
        {
776
            return pathinfo($filename, PATHINFO_FILENAME);
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($filenam...tils\PATHINFO_FILENAME) could return the type array which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
777
        }
778
        
779
        $parts = explode('/', $filename);
780
        
781
        $file = self::removeExtension(array_pop($parts));
782
        
783
        $parts[] = $file;
784
        
785
        return implode('/', $parts);
786
    }
787
788
    /**
789
     * Detects the UTF BOM in the target file, if any. Returns
790
     * the encoding matching the BOM, which can be any of the
791
     * following:
792
     *
793
     * <ul>
794
     * <li>UTF32-BE</li>
795
     * <li>UTF32-LE</li>
796
     * <li>UTF16-BE</li>
797
     * <li>UTF16-LE</li>
798
     * <li>UTF8</li>
799
     * </ul>
800
     *
801
     * @param string $filename
802
     * @return string|NULL
803
     * @throws FileHelper_Exception
804
     *
805
     * @see FileHelper::ERROR_CANNOT_OPEN_FILE_TO_DETECT_BOM
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
   /**
837
    * Retrieves a list of all UTF byte order mark character
838
    * sequences, as an associative array with UTF encoding => bom sequence
839
    * pairs.
840
    * 
841
    * @return array<string,string>
842
    */
843
    public static function getUTFBOMs() : array
844
    {
845
        if(!isset(self::$utfBoms)) {
846
            self::$utfBoms = array(
847
                'UTF32-BE' => chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF),
848
                'UTF32-LE' => chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00),
849
                'UTF16-BE' => chr(0xFE) . chr(0xFF),
850
                'UTF16-LE' => chr(0xFF) . chr(0xFE),
851
                'UTF8' => chr(0xEF) . chr(0xBB) . chr(0xBF)
852
            );
853
        }
854
        
855
        return self::$utfBoms;
856
    }
857
    
858
   /**
859
    * Checks whether the specified encoding is a valid
860
    * unicode encoding, for example "UTF16-LE" or "UTF8".
861
    * Also accounts for alternate way to write the, like
862
    * "UTF-8", and omitting little/big endian suffixes.
863
    * 
864
    * @param string $encoding
865
    * @return boolean
866
    */
867
    public static function isValidUnicodeEncoding(string $encoding) : bool
868
    {
869
        $encodings = self::getKnownUnicodeEncodings();
870
871
        $keep = array();
872
        foreach($encodings as $string) 
873
        {
874
            $withHyphen = str_replace('UTF', 'UTF-', $string);
875
            
876
            $keep[] = $string;
877
            $keep[] = $withHyphen; 
878
            $keep[] = str_replace(array('-BE', '-LE'), '', $string);
879
            $keep[] = str_replace(array('-BE', '-LE'), '', $withHyphen);
880
        }
881
        
882
        return in_array($encoding, $keep);
883
    }
884
    
885
   /**
886
    * Retrieves a list of all known unicode file encodings.
887
    * @return string[]
888
    */
889
    public static function getKnownUnicodeEncodings() : array
890
    {
891
        return array_keys(self::getUTFBOMs());
892
    }
893
    
894
   /**
895
    * Normalizes the slash style in a file or folder path,
896
    * by replacing any antislashes with forward slashes.
897
    * 
898
    * @param string $path
899
    * @return string
900
    */
901
    public static function normalizePath(string $path) : string
902
    {
903
        return str_replace(array('\\', '//'), array('/', '/'), $path);
904
    }
905
    
906
   /**
907
    * Saves the specified data to a file, JSON encoded.
908
    * 
909
    * @param mixed $data
910
    * @param string $file
911
    * @param bool $pretty
912
    * @throws FileHelper_Exception
913
    * 
914
    * @see FileHelper::ERROR_JSON_ENCODE_ERROR
915
    * @see FileHelper::ERROR_SAVE_FOLDER_NOT_WRITABLE
916
    * @see FileHelper::ERROR_SAVE_FILE_NOT_WRITABLE
917
    * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
918
    */
919
    public static function saveAsJSON($data, string $file, bool $pretty=false) : void
920
    {
921
        $options = null;
922
        if($pretty) {
923
            $options = JSON_PRETTY_PRINT;
924
        }
925
        
926
        $json = json_encode($data, $options);
0 ignored issues
show
Bug introduced by
It seems like $options can also be of type null; however, parameter $flags of json_encode() does only seem to accept integer, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

926
        $json = json_encode($data, /** @scrutinizer ignore-type */ $options);
Loading history...
927
        
928
        if($json===false) 
929
        {
930
            $errorCode = json_last_error();
931
            
932
            throw new FileHelper_Exception(
933
                'An error occurred while encdoding a data set to JSON. Native error message: ['.json_last_error_msg().'].', 
934
                'JSON error code: '.$errorCode,
935
                self::ERROR_JSON_ENCODE_ERROR
936
            ); 
937
        }
938
        
939
        self::saveFile($file, $json);
940
    }
941
   
942
   /**
943
    * Saves the specified content to the target file, creating
944
    * the file and the folder as necessary.
945
    * 
946
    * @param string $filePath
947
    * @param string $content
948
    * @throws FileHelper_Exception
949
    * 
950
    * @see FileHelper::ERROR_SAVE_FOLDER_NOT_WRITABLE
951
    * @see FileHelper::ERROR_SAVE_FILE_NOT_WRITABLE
952
    * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
953
    */
954
    public static function saveFile(string $filePath, string $content='') : void
955
    {
956
        $filePath = self::normalizePath($filePath);
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(is_dir($filePath))
995
        {
996
            throw new FileHelper_Exception(
997
                sprintf('Cannot save file: the target [%s] is a directory.', basename($filePath)),
998
                sprintf(
999
                    'Tried saving content to path [%s].',
1000
                    $filePath
1001
                ),
1002
                self::ERROR_CANNOT_WRITE_TO_FOLDER
1003
            );
1004
        }
1005
        
1006
        if(file_put_contents($filePath, $content) !== false) {
1007
            return;
1008
        }
1009
        
1010
        throw new FileHelper_Exception(
1011
            sprintf('Cannot save file: writing content to the file [%s] failed.', basename($filePath)),
1012
            sprintf(
1013
                'Tried saving content to file in path [%s].',
1014
                $filePath
1015
            ),
1016
            self::ERROR_SAVE_FILE_WRITE_FAILED
1017
        );
1018
    }
1019
1020
    /**
1021
     * Checks whether it is possible to run PHP command
1022
     * line commands.
1023
     *
1024
     * @return boolean
1025
     * @throws FileHelper_Exception
1026
     */
1027
    public static function canMakePHPCalls() : bool
1028
    {
1029
        return self::cliCommandExists('php');
1030
    }
1031
    
1032
    /**
1033
     * Determines if a command exists on the current environment's command line interface.
1034
     *
1035
     * @param string $command The name of the command to check, e.g. "php"
1036
     * @return bool True if the command has been found, false otherwise.
1037
     * @throws FileHelper_Exception 
1038
     * 
1039
     * @todo Move this to a separate class.
1040
     */
1041
    public static  function cliCommandExists(string $command) : bool
1042
    {
1043
        static $checked = array();
1044
        
1045
        if(isset($checked[$command])) {
1046
            return $checked[$command];
1047
        }
1048
        
1049
        // command to use to search for available commands
1050
        // on the target OS
1051
        $osCommands = array(
1052
            'windows' => 'where',
1053
            'linux' => 'which'
1054
        );
1055
        
1056
        $os = strtolower(PHP_OS_FAMILY);
1057
        
1058
        if(!isset($osCommands[$os])) 
1059
        {
1060
            throw new FileHelper_Exception(
1061
                'Unsupported OS for CLI commands',
1062
                sprintf(
1063
                    'The command to search for available CLI commands is not known for the OS [%s].',
1064
                    $os
1065
                ),
1066
                self::ERROR_UNSUPPORTED_OS_CLI_COMMAND
1067
            );
1068
        }
1069
        
1070
        $whereCommand = $osCommands[$os];
1071
        
1072
        $pipes = array();
1073
        
1074
        $process = proc_open(
1075
            $whereCommand.' '.$command,
1076
            array(
1077
                0 => array("pipe", "r"), //STDIN
1078
                1 => array("pipe", "w"), //STDOUT
1079
                2 => array("pipe", "w"), //STDERR
1080
            ),
1081
            $pipes
1082
        );
1083
        
1084
        if($process === false) {
1085
            $checked[$command] = false;
1086
            return false;
1087
        }
1088
        
1089
        $stdout = stream_get_contents($pipes[1]);
1090
        
1091
        fclose($pipes[1]);
1092
        fclose($pipes[2]);
1093
        
1094
        proc_close($process);
1095
        
1096
        $result = $stdout != '';
1097
        
1098
        $checked[$command] = $result;
1099
        
1100
        return $result;
1101
    }
1102
1103
    /**
1104
     * Validates a PHP file's syntax.
1105
     *
1106
     * NOTE: This will fail silently if the PHP command line
1107
     * is not available. Use {@link FileHelper::canMakePHPCalls()}
1108
     * to check this beforehand as needed.
1109
     *
1110
     * @param string $path
1111
     * @return boolean|array A boolean true if the file is valid, an array with validation messages otherwise.
1112
     * @throws FileHelper_Exception
1113
     */
1114
    public static function checkPHPFileSyntax(string $path)
1115
    {
1116
        if(!self::canMakePHPCalls()) {
1117
            return true;
1118
        }
1119
        
1120
        $output = array();
1121
        $command = sprintf('php -l "%s" 2>&1', $path);
1122
        exec($command, $output);
1123
        
1124
        // when the validation is successful, the first entry
1125
        // in the array contains the success message. When it
1126
        // is invalid, the first entry is always empty.
1127
        if(!empty($output[0])) {
1128
            return true;
1129
        }
1130
        
1131
        array_shift($output); // the first entry is always empty
1132
        array_pop($output); // the last message is a superfluous message saying there's an error
1133
        
1134
        return $output;
1135
    }
1136
    
1137
   /**
1138
    * Retrieves the last modified date for the specified file or folder.
1139
    * 
1140
    * Note: If the target does not exist, returns null. 
1141
    * 
1142
    * @param string $path
1143
    * @return DateTime|NULL
1144
    */
1145
    public static function getModifiedDate(string $path) : ?DateTime
1146
    {
1147
        $time = filemtime($path);
1148
        if($time === false) {
1149
            return null;
1150
        }
1151
1152
        $date = new DateTime();
1153
        $date->setTimestamp($time);
1154
        return $date;
1155
    }
1156
    
1157
   /**
1158
    * Retrieves the names of all subfolders in the specified path.
1159
    * 
1160
    * Available options:
1161
    * 
1162
    * - recursive: true/false
1163
    *   Whether to search for subfolders recursively. 
1164
    *   
1165
    * - absolute-paths: true/false
1166
    *   Whether to return a list of absolute paths.
1167
    * 
1168
    * @param string|DirectoryIterator $targetFolder
1169
    * @param array $options
1170
    * @throws FileHelper_Exception
1171
    * @return string[]
1172
    *
1173
    * @see FileHelper::ERROR_FIND_SUBFOLDERS_FOLDER_DOES_NOT_EXIST
1174
    * @todo Move this to a separate class.
1175
    */
1176
    public static function getSubfolders($targetFolder, $options = array())
1177
    {
1178
        if($targetFolder instanceof DirectoryIterator) {
1179
            $targetFolder = $targetFolder->getPathname();
1180
        }
1181
1182
        if(!is_dir($targetFolder)) 
1183
        {
1184
            throw new FileHelper_Exception(
1185
                'Target folder does not exist',
1186
                sprintf(
1187
                    'Cannot retrieve subfolders from [%s], the folder does not exist.',
1188
                    $targetFolder
1189
                ),
1190
                self::ERROR_FIND_SUBFOLDERS_FOLDER_DOES_NOT_EXIST
1191
            );
1192
        }
1193
        
1194
        $options = array_merge(
1195
            array(
1196
                'recursive' => false,
1197
                'absolute-path' => false
1198
            ), 
1199
            $options
1200
        );
1201
        
1202
        $result = array();
1203
        
1204
        $d = new DirectoryIterator($targetFolder);
1205
        
1206
        foreach($d as $item) 
1207
        {
1208
            if($item->isDir() && !$item->isDot()) 
1209
            {
1210
                $name = $item->getFilename();
1211
                
1212
                if(!$options['absolute-path']) {
1213
                    $result[] = $name;
1214
                } else {
1215
                    $result[] = $targetFolder.'/'.$name;
1216
                }
1217
                
1218
                if(!$options['recursive']) 
1219
                {
1220
                    continue;
1221
                }
1222
                
1223
                $subs = self::getSubfolders($targetFolder.'/'.$name, $options);
1224
                foreach($subs as $sub) 
1225
                {
1226
                    $relative = $name.'/'.$sub;
1227
                    
1228
                    if(!$options['absolute-path']) {
1229
                        $result[] = $relative;
1230
                    } else {
1231
                        $result[] = $targetFolder.'/'.$relative;
1232
                    }
1233
                }
1234
            }
1235
        }
1236
        
1237
        return $result;
1238
    }
1239
1240
   /**
1241
    * Retrieves the maximum allowed upload file size, in bytes.
1242
    * Takes into account the PHP ini settings <code>post_max_size</code>
1243
    * and <code>upload_max_filesize</code>. Since these cannot
1244
    * be modified at runtime, they are the hard limits for uploads.
1245
    * 
1246
    * NOTE: Based on binary values, where 1KB = 1024 Bytes.
1247
    * 
1248
    * @return int Will return <code>-1</code> if no limit.
1249
    */
1250
    public static function getMaxUploadFilesize() : int
1251
    {
1252
        static $max_size = -1;
1253
        
1254
        if ($max_size < 0)
1255
        {
1256
            // Start with post_max_size.
1257
            $post_max_size = self::parse_size(ini_get('post_max_size'));
1258
            if ($post_max_size > 0) {
1259
                $max_size = $post_max_size;
1260
            }
1261
            
1262
            // If upload_max_size is less, then reduce. Except if upload_max_size is
1263
            // zero, which indicates no limit.
1264
            $upload_max = self::parse_size(ini_get('upload_max_filesize'));
1265
            if ($upload_max > 0 && $upload_max < $max_size) {
1266
                $max_size = $upload_max;
1267
            }
1268
        }
1269
        
1270
        return $max_size;
1271
    }
1272
    
1273
    protected static function parse_size(string $size) : float
1274
    {
1275
        $unit = preg_replace('/[^bkmgtpezy]/i', '', $size); // Remove the non-unit characters from the size.
1276
        $size = floatval(preg_replace('/[^0-9\.]/', '', $size)); // Remove the non-numeric characters from the size.
1277
        
1278
        if($unit) 
1279
        {
1280
            // Find the position of the unit in the ordered string which is the power of magnitude to multiply a kilobyte by.
1281
            return round($size * pow(1024, stripos('bkmgtpezy', $unit[0])));
1282
        }
1283
        
1284
        return round($size);
1285
    }
1286
   
1287
   /**
1288
    * Makes a path relative using a folder depth: will reduce the
1289
    * length of the path so that only the amount of folders defined
1290
    * in the <code>$depth</code> attribute are shown below the actual
1291
    * folder or file in the path.
1292
    *  
1293
    * @param string  $path The absolute or relative path
1294
    * @param int $depth The folder depth to reduce the path to
1295
    * @return string
1296
    */
1297
    public static function relativizePathByDepth(string $path, int $depth=2) : string
1298
    {
1299
        $path = self::normalizePath($path);
1300
        
1301
        $tokens = explode('/', $path);
1302
        $tokens = array_filter($tokens); // remove empty entries (trailing slash for example)
1303
        $tokens = array_values($tokens); // re-index keys
1304
        
1305
        if(empty($tokens)) {
1306
            return '';
1307
        }
1308
        
1309
        // remove the drive if present
1310
        if(strstr($tokens[0], ':')) {
1311
            array_shift($tokens);
1312
        }
1313
        
1314
        // path was only the drive
1315
        if(count($tokens) == 0) {
1316
            return '';
1317
        }
1318
1319
        // the last element (file or folder)
1320
        $target = array_pop($tokens);
1321
        
1322
        // reduce the path to the specified depth
1323
        $length = count($tokens);
1324
        if($length > $depth) {
1325
            $tokens = array_slice($tokens, $length-$depth);
1326
        }
1327
1328
        // append the last element again
1329
        $tokens[] = $target;
1330
        
1331
        return trim(implode('/', $tokens), '/');
1332
    }
1333
    
1334
   /**
1335
    * Makes the specified path relative to another path,
1336
    * by removing one from the other if found. Also 
1337
    * normalizes the path to use forward slashes. 
1338
    * 
1339
    * Example:
1340
    * 
1341
    * <pre>
1342
    * relativizePath('c:\some\folder\to\file.txt', 'c:\some\folder');
1343
    * </pre>
1344
    * 
1345
    * Result: <code>to/file.txt</code>
1346
    * 
1347
    * @param string $path
1348
    * @param string $relativeTo
1349
    * @return string
1350
    */
1351
    public static function relativizePath(string $path, string $relativeTo) : string
1352
    {
1353
        $path = self::normalizePath($path);
1354
        $relativeTo = self::normalizePath($relativeTo);
1355
        
1356
        $relative = str_replace($relativeTo, '', $path);
1357
        $relative = trim($relative, '/');
1358
        
1359
        return $relative;
1360
    }
1361
    
1362
   /**
1363
    * Checks that the target file exists, and throws an exception
1364
    * if it does not. 
1365
    * 
1366
    * @param string $path
1367
    * @param int|NULL $errorCode Optional custom error code
1368
    * @throws FileHelper_Exception
1369
    * @return string The real path to the file
1370
    * 
1371
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1372
    */
1373
    public static function requireFileExists(string $path, ?int $errorCode=null) : string
1374
    {
1375
        $result = realpath($path);
1376
        if($result !== false) {
1377
            return $result;
1378
        }
1379
        
1380
        if($errorCode === null) {
1381
            $errorCode = self::ERROR_FILE_DOES_NOT_EXIST;
1382
        }
1383
        
1384
        throw new FileHelper_Exception(
1385
            sprintf('File [%s] does not exist.', basename($path)),
1386
            sprintf('Tried finding the file in path [%s].', $path),
1387
            $errorCode
1388
        );
1389
    }
1390
1391
    /**
1392
     * @param string $path
1393
     * @param int|NULL $errorCode
1394
     * @return string
1395
     * @throws FileHelper_Exception
1396
     */
1397
    public static function requireFileReadable(string $path, ?int $errorCode=null) : string
1398
    {
1399
        $path = self::requireFileExists($path, $errorCode);
1400
1401
        if(is_readable($path)) {
1402
            return $path;
1403
        }
1404
1405
        if($errorCode === null) {
0 ignored issues
show
introduced by
The condition $errorCode === null is always false.
Loading history...
1406
            $errorCode = self::ERROR_FILE_NOT_READABLE;
1407
        }
1408
1409
        throw new FileHelper_Exception(
1410
            sprintf('File [%s] is not readable.', basename($path)),
1411
            sprintf('Tried accessing the file in path [%s].', $path),
1412
            $errorCode
1413
        );
1414
    }
1415
    
1416
   /**
1417
    * Reads a specific line number from the target file and returns its
1418
    * contents, if the file has such a line. Does so with little memory
1419
    * usage, as the file is not read entirely into memory.
1420
    * 
1421
    * @param string $path
1422
    * @param int $lineNumber Note: 1-based; the first line is number 1.
1423
    * @return string|NULL Will return null if the requested line does not exist.
1424
    * @throws FileHelper_Exception
1425
    * 
1426
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1427
    */
1428
    public static function getLineFromFile(string $path, int $lineNumber) : ?string
1429
    {
1430
        self::requireFileExists($path);
1431
        
1432
        $file = new \SplFileObject($path);
1433
        
1434
        if($file->eof()) {
1435
            return '';
1436
        }
1437
        
1438
        $targetLine = $lineNumber-1;
1439
        
1440
        $file->seek($targetLine);
1441
        
1442
        if($file->key() !== $targetLine) {
1443
             return null;
1444
        }
1445
        
1446
        return $file->current(); 
0 ignored issues
show
Bug Best Practice introduced by
The expression return $file->current() could return the type array which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
1447
    }
1448
    
1449
   /**
1450
    * Retrieves the total amount of lines in the file, without 
1451
    * reading the whole file into memory.
1452
    * 
1453
    * @param string $path
1454
    * @return int
1455
    */
1456
    public static function countFileLines(string $path) : int
1457
    {
1458
        self::requireFileExists($path);
1459
        
1460
        $spl = new \SplFileObject($path);
1461
        
1462
        // tries seeking as far as possible
1463
        $spl->seek(PHP_INT_MAX);
1464
        
1465
        $number = $spl->key();
1466
        
1467
        // if seeking to the end the cursor is still at 0, there are no lines. 
1468
        if($number === 0) 
1469
        {
1470
            // since it's a very small file, to get reliable results,
1471
            // we read its contents and use that to determine what
1472
            // kind of contents we are dealing with. Tests have shown 
1473
            // that this is not pactical to solve with the SplFileObject.
1474
            $content = file_get_contents($path);
1475
            
1476
            if(empty($content)) {
1477
                return 0;
1478
            }
1479
        }
1480
        
1481
        // return the line number we were able to reach + 1 (key is zero-based)
1482
        return $number+1;
1483
    }
1484
    
1485
   /**
1486
    * Parses the target file to detect any PHP classes contained
1487
    * within, and retrieve information on them. Does not use the 
1488
    * PHP reflection API.
1489
    * 
1490
    * @param string $filePath
1491
    * @return FileHelper_PHPClassInfo
1492
    */
1493
    public static function findPHPClasses(string $filePath) : FileHelper_PHPClassInfo
1494
    {
1495
        return new FileHelper_PHPClassInfo($filePath);
1496
    }
1497
    
1498
   /**
1499
    * Detects the end of line style used in the target file, if any.
1500
    * Can be used with large files, because it only reads part of it.
1501
    * 
1502
    * @param string $filePath The path to the file.
1503
    * @return NULL|ConvertHelper_EOL The end of line character information, or NULL if none is found.
1504
    */
1505
    public static function detectEOLCharacter(string $filePath) : ?ConvertHelper_EOL
1506
    {
1507
        // 20 lines is enough to get a good picture of the newline style in the file.
1508
        $amount = 20;
1509
        
1510
        $lines = self::readLines($filePath, $amount);
1511
        
1512
        $string = implode('', $lines);
1513
        
1514
        return ConvertHelper::detectEOLCharacter($string);
1515
    }
1516
    
1517
   /**
1518
    * Reads the specified amount of lines from the target file.
1519
    * Unicode BOM compatible: any byte order marker is stripped
1520
    * from the resulting lines.
1521
    * 
1522
    * @param string $filePath
1523
    * @param int $amount Set to 0 to read all lines.
1524
    * @return array
1525
    * 
1526
    * @see FileHelper::ERROR_CANNOT_OPEN_FILE_TO_READ_LINES
1527
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1528
    */
1529
    public static function readLines(string $filePath, int $amount=0) : array
1530
    {
1531
        self::requireFileExists($filePath);
1532
        
1533
        $fn = fopen($filePath, "r");
1534
        
1535
        if($fn === false) 
1536
        {
1537
            throw new FileHelper_Exception(
1538
                'Could not open file for reading.',
1539
                sprintf(
1540
                    'Tried accessing file at [%s].',
1541
                    $filePath
1542
                ),
1543
                self::ERROR_CANNOT_OPEN_FILE_TO_READ_LINES
1544
            );
1545
        }
1546
        
1547
        $result = array();
1548
        $counter = 0;
1549
        $first = true;
1550
        
1551
        while(!feof($fn)) 
1552
        {
1553
            $counter++;
1554
            
1555
            $line = fgets($fn);
1556
            
1557
            // can happen with zero length files
1558
            if($line === false) {
1559
                continue;
1560
            }
1561
            
1562
            // the first line may contain a unicode BOM marker.
1563
            if($first) 
1564
            {
1565
                $line = ConvertHelper::stripUTFBom($line);
1566
                $first = false;
1567
            }
1568
            
1569
            $result[] = $line;
1570
            
1571
            if($amount > 0 && $counter == $amount) {
1572
                break;
1573
            }
1574
        }
1575
        
1576
        fclose($fn);
1577
        
1578
        return $result;
1579
    }
1580
    
1581
   /**
1582
    * Reads all content from a file.
1583
    * 
1584
    * @param string $filePath
1585
    * @throws FileHelper_Exception
1586
    * @return string
1587
    * 
1588
    * @see FileHelper::ERROR_FILE_DOES_NOT_EXIST
1589
    * @see FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
1590
    */
1591
    public static function readContents(string $filePath) : string
1592
    {
1593
        self::requireFileExists($filePath);
1594
        
1595
        $result = file_get_contents($filePath);
1596
        
1597
        if($result !== false) {
1598
            return $result;
1599
        }
1600
        
1601
        throw new FileHelper_Exception(
1602
            sprintf('Cannot read contents of file [%s].', basename($filePath)),
1603
            sprintf(
1604
                'Tried opening file for reading at: [%s].',
1605
                $filePath
1606
            ),
1607
            self::ERROR_CANNOT_READ_FILE_CONTENTS
1608
        );
1609
    }
1610
1611
   /**
1612
    * Ensures that the target path exists on disk, and is a folder.
1613
    * 
1614
    * @param string $path
1615
    * @return string The real path, with normalized slashes.
1616
    * @throws FileHelper_Exception
1617
    * 
1618
    * @see FileHelper::normalizePath()
1619
    * 
1620
    * @see FileHelper::ERROR_FOLDER_DOES_NOT_EXIST
1621
    * @see FileHelper::ERROR_PATH_IS_NOT_A_FOLDER
1622
    */
1623
    public static function requireFolderExists(string $path) : string
1624
    {
1625
        $actual = realpath($path);
1626
        
1627
        if($actual === false) 
1628
        {
1629
            throw new FileHelper_Exception(
1630
                'Folder does not exist',
1631
                sprintf(
1632
                    'The path [%s] does not exist on disk.',
1633
                    $path
1634
                ),
1635
                self::ERROR_FOLDER_DOES_NOT_EXIST
1636
            );
1637
        }
1638
        
1639
        if(is_dir($path)) 
1640
        {
1641
            return self::normalizePath($actual);
1642
        }
1643
        
1644
        throw new FileHelper_Exception(
1645
            'Target is not a folder',
1646
            sprintf(
1647
                'The path [%s] does not point to a folder.',
1648
                $path
1649
            ),
1650
            self::ERROR_PATH_IS_NOT_A_FOLDER
1651
        );
1652
    }
1653
1654
    /**
1655
     * Creates an instance of the paths reducer tool, which can reduce
1656
     * a list of paths to the closest common root folder.
1657
     *
1658
     * @param string[] $paths
1659
     * @return FileHelper_PathsReducer
1660
     */
1661
    public static function createPathsReducer(array $paths=array()) : FileHelper_PathsReducer
1662
    {
1663
        return new FileHelper_PathsReducer();
1664
    }
1665
}
1666