Passed
Push — master ( 68f91e...986f6c )
by Sebastian
04:32
created

FileHelper   F

Complexity

Total Complexity 148

Size/Duplication

Total Lines 1589
Duplicated Lines 0 %

Importance

Changes 24
Bugs 1 Features 0
Metric Value
wmc 148
eloc 552
c 24
b 1
f 0
dl 0
loc 1589
rs 2

48 Methods

Rating   Name   Duplication   Size   Complexity  
A requireFileReadable() 0 16 3
B saveFile() 0 63 6
A parseJSONFile() 0 33 5
A createFolder() 0 10 3
A deleteFile() 0 17 3
A parseCSVString() 0 13 2
A getKnownUnicodeEncodings() 0 3 1
A copyFile() 0 36 4
A createCSVParser() 0 10 3
B readLines() 0 50 7
A normalizePath() 0 3 1
A getModifiedDate() 0 10 2
A sendFile() 0 36 4
A copyTree() 0 22 5
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 parseSerializedFile() 0 31 3
A detectMimeType() 0 8 2
A saveAsJSON() 0 21 3
B getSubfolders() 0 58 9
B deleteTree() 0 32 9
A canMakePHPCalls() 0 3 1
A requireFolderExists() 0 28 3
A requireFileExists() 0 15 3
A openUnserialized() 0 3 1
A getMaxUploadFilesize() 0 21 5
A readContents() 0 17 2
A findPHPFiles() 0 3 1
A findHTMLFiles() 0 3 1
A relativizePathByDepth() 0 35 5
A checkPHPFileSyntax() 0 21 3
A countFileLines() 0 27 3
A createFileFinder() 0 3 1
A getFilename() 0 14 3
A findPHPClasses() 0 3 1
A detectEOLCharacter() 0 10 1
A cliCommandExists() 0 60 4
A detectUTFBom() 0 27 4
A relativizePath() 0 9 1
A getUTFBOMs() 0 13 2
A parseCSVFile() 0 5 1
A findFiles() 0 23 6
A parse_size() 0 12 2
A removeExtension() 0 17 2

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

528
        	$ext = mb_strtolower(/** @scrutinizer ignore-type */ $ext);
Loading history...
529
        }
530
        
531
        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...
532
    }
533
    
534
   /**
535
    * Retrieves the file name from a path, with or without extension.
536
    * The path to the file can be a string, or a DirectoryIterator object
537
    * instance.
538
    * 
539
    * In case of folders, behaves like the pathinfo function: returns
540
    * the name of the folder.
541
    * 
542
    * @param string|\DirectoryIterator $pathOrDirIterator
543
    * @param bool $extension
544
    * @return string
545
    */
546
    public static function getFilename($pathOrDirIterator, $extension = true)
547
    {
548
        $path = $pathOrDirIterator;
549
    	if($pathOrDirIterator instanceof \DirectoryIterator) {
550
    		$path = $pathOrDirIterator->getFilename();
551
    	}
552
    	
553
    	$path = self::normalizePath($path);
554
    	
555
    	if(!$extension) {
556
    	    return pathinfo($path, PATHINFO_FILENAME);
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($path, AppUtils\PATHINFO_FILENAME) also could return the type array which is incompatible with the documented return type string.
Loading history...
557
    	}
558
    	
559
    	return pathinfo($path, PATHINFO_BASENAME); 
0 ignored issues
show
Bug Best Practice introduced by
The expression return pathinfo($path, AppUtils\PATHINFO_BASENAME) also could return the type array which is incompatible with the documented return type string.
Loading history...
560
    }
561
   
562
   /**
563
    * Tries to read the contents of the target file and
564
    * treat it as JSON to return the decoded JSON data.
565
    * 
566
    * @param string $file
567
    * @throws FileHelper_Exception
568
    * @return array
569
    * 
570
    * @see FileHelper::ERROR_CANNOT_FIND_JSON_FILE
571
    * @see FileHelper::ERROR_CANNOT_DECODE_JSON_FILE
572
    */ 
573
    public static function parseJSONFile(string $file, $targetEncoding=null, $sourceEncoding=null)
574
    {
575
        self::requireFileExists($file, self::ERROR_CANNOT_FIND_JSON_FILE);
576
        
577
        $content = file_get_contents($file);
578
        if(!$content) {
579
            throw new FileHelper_Exception(
580
                'Cannot get file contents',
581
                sprintf(
582
                    'The file [%s] exists on disk, but its contents cannot be read.',
583
                    $file    
584
                ),
585
                self::ERROR_JSON_FILE_CANNOT_BE_READ
586
            );
587
        }
588
        
589
        if(isset($targetEncoding)) {
590
            $content = mb_convert_encoding($content, $targetEncoding, $sourceEncoding);
591
        }
592
        
593
        $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

593
        $json = json_decode(/** @scrutinizer ignore-type */ $content, true);
Loading history...
594
        if($json === false || $json === NULL) {
595
            throw new FileHelper_Exception(
596
                'Cannot decode json data',
597
                sprintf(
598
                    'Loaded the contents of file [%s] successfully, but decoding it as JSON failed.',
599
                    $file    
600
                ),
601
                self::ERROR_CANNOT_DECODE_JSON_FILE
602
            );
603
        }
604
        
605
        return $json;
606
    }
607
    
608
   /**
609
    * Corrects common formatting mistakes when users enter
610
    * file names, like too many spaces, dots and the like.
611
    * 
612
    * NOTE: if the file name contains a path, the path is
613
    * stripped, leaving only the file name.
614
    * 
615
    * @param string $name
616
    * @return string
617
    */
618
    public static function fixFileName(string $name) : string
619
    {
620
        $name = trim($name);
621
        $name = self::normalizePath($name);
622
        $name = basename($name);
623
        
624
        $replaces = array(
625
            "\t" => ' ',
626
            "\r" => ' ',
627
            "\n" => ' ',
628
            ' .' => '.',
629
            '. ' => '.',
630
        );
631
        
632
        $name = str_replace(array_keys($replaces), array_values($replaces), $name);
633
        
634
        while(strstr($name, '  ')) {
635
            $name = str_replace('  ', ' ', $name);
636
        }
637
638
        $name = str_replace(array_keys($replaces), array_values($replaces), $name);
639
        
640
        while(strstr($name, '..')) {
641
            $name = str_replace('..', '.', $name);
642
        }
643
        
644
        return $name;
645
    }
646
    
647
   /**
648
    * Creates an instance of the file finder, which is an easier
649
    * alternative to the other manual findFile methods, since all
650
    * options can be set by chaining.
651
    * 
652
    * @param string $path
653
    * @return FileHelper_FileFinder
654
    */
655
    public static function createFileFinder(string $path) : FileHelper_FileFinder
656
    {
657
        return new FileHelper_FileFinder($path);
658
    }
659
    
660
   /**
661
    * Searches for all HTML files in the target folder.
662
    * 
663
    * NOTE: This method only exists for backwards compatibility.
664
    * Use the `createFileFinder()` method instead, which offers
665
    * an object oriented interface that is much easier to use.
666
    * 
667
    * @param string $targetFolder
668
    * @param array $options
669
    * @return array An indexed array with files.
670
    * @see FileHelper::createFileFinder()
671
    */
672
    public static function findHTMLFiles(string $targetFolder, array $options=array()) : array
673
    {
674
        return self::findFiles($targetFolder, array('html'), $options);
675
    }
676
677
   /**
678
    * Searches for all PHP files in the target folder.
679
    * 
680
    * NOTE: This method only exists for backwards compatibility.
681
    * Use the `createFileFinder()` method instead, which offers
682
    * an object oriented interface that is much easier to use.
683
    * 
684
    * @param string $targetFolder
685
    * @param array $options
686
    * @return array An indexed array of PHP files.
687
    * @see FileHelper::createFileFinder()
688
    */
689
    public static function findPHPFiles(string $targetFolder, array $options=array()) : array
690
    {
691
        return self::findFiles($targetFolder, array('php'), $options);
692
    }
693
    
694
   /**
695
    * Finds files according to the specified options.
696
    * 
697
    * NOTE: This method only exists for backwards compatibility.
698
    * Use the `createFileFinder()` method instead, which offers
699
    * an object oriented interface that is much easier to use.
700
    *  
701
    * @param string $targetFolder
702
    * @param array $extensions
703
    * @param array $options
704
    * @param array $files
705
    * @throws FileHelper_Exception
706
    * @return array
707
    * @see FileHelper::createFileFinder()
708
    */
709
    public static function findFiles(string $targetFolder, array $extensions=array(), array $options=array(), array $files=array()) : array
710
    {
711
        $finder = self::createFileFinder($targetFolder);
712
713
        $finder->setPathmodeStrip();
714
        
715
        if(isset($options['relative-path']) && $options['relative-path'] === true) 
716
        {
717
            $finder->setPathmodeRelative();
718
        } 
719
        else if(isset($options['absolute-path']) && $options['absolute-path'] === true)
720
        {
721
            $finder->setPathmodeAbsolute();
722
        }
723
        
724
        if(isset($options['strip-extension'])) 
725
        {
726
            $finder->stripExtensions();
727
        }
728
        
729
        $finder->setOptions($options);
730
        
731
        return $finder->getAll();
732
    }
733
734
   /**
735
    * Removes the extension from the specified path or file name,
736
    * if any, and returns the name without the extension.
737
    * 
738
    * @param string $filename
739
    * @param bool $keepPath Whether to keep the path component, if any. Default PHP pathinfo behavior is not to.
740
    * @return string
741
    */
742
    public static function removeExtension(string $filename, bool $keepPath=false) : string
743
    {
744
        // normalize paths to allow windows style slashes even on nix servers
745
        $filename = self::normalizePath($filename);
746
        
747
        if(!$keepPath) 
748
        {
749
            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...
750
        }
751
        
752
        $parts = explode('/', $filename);
753
        
754
        $file = self::removeExtension(array_pop($parts));
755
        
756
        $parts[] = $file;
757
        
758
        return implode('/', $parts);
759
    }
760
    
761
   /**
762
    * Detects the UTF BOM in the target file, if any. Returns
763
    * the encoding matching the BOM, which can be any of the
764
    * following:
765
    * 
766
    * <ul>
767
    * <li>UTF32-BE</li>
768
    * <li>UTF32-LE</li>
769
    * <li>UTF16-BE</li>
770
    * <li>UTF16-LE</li>
771
    * <li>UTF8</li>
772
    * </ul>
773
    * 
774
    * @param string $filename
775
    * @return string|NULL
776
    */
777
    public static function detectUTFBom(string $filename) : ?string
778
    {
779
        $fp = fopen($filename, 'r');
780
        if($fp === false) 
781
        {
782
            throw new FileHelper_Exception(
783
                'Cannot open file for reading',
784
                sprintf('Tried opening file [%s] in read mode.', $filename),
785
                self::ERROR_CANNOT_OPEN_FILE_TO_DETECT_BOM
786
            );
787
        }
788
        
789
        $text = fread($fp, 20);
790
        
791
        fclose($fp);
792
793
        $boms = self::getUTFBOMs();
794
        
795
        foreach($boms as $bom => $value) 
796
        {
797
            $length = mb_strlen($value);
798
            if(mb_substr($text, 0, $length) == $value) {
799
                return $bom;
800
            }
801
        }
802
        
803
        return null;
804
    }    
805
    
806
    protected static $utfBoms;
807
    
808
   /**
809
    * Retrieves a list of all UTF byte order mark character
810
    * sequences, as an assocative array with UTF encoding => bom sequence
811
    * pairs.
812
    * 
813
    * @return array
814
    */
815
    public static function getUTFBOMs()
816
    {
817
        if(!isset(self::$utfBoms)) {
818
            self::$utfBoms = array(
819
                'UTF32-BE' => chr(0x00) . chr(0x00) . chr(0xFE) . chr(0xFF),
820
                'UTF32-LE' => chr(0xFF) . chr(0xFE) . chr(0x00) . chr(0x00),
821
                'UTF16-BE' => chr(0xFE) . chr(0xFF),
822
                'UTF16-LE' => chr(0xFF) . chr(0xFE),
823
                'UTF8' => chr(0xEF) . chr(0xBB) . chr(0xBF)
824
            );
825
        }
826
        
827
        return self::$utfBoms;
828
    }
829
    
830
   /**
831
    * Checks whether the specified encoding is a valid
832
    * unicode encoding, for example "UTF16-LE" or "UTF8".
833
    * Also accounts for alternate way to write the, like
834
    * "UTF-8", and omitting little/big endian suffixes.
835
    * 
836
    * @param string $encoding
837
    * @return boolean
838
    */
839
    public static function isValidUnicodeEncoding(string $encoding) : bool
840
    {
841
        $encodings = self::getKnownUnicodeEncodings();
842
843
        $keep = array();
844
        foreach($encodings as $string) 
845
        {
846
            $withHyphen = str_replace('UTF', 'UTF-', $string);
847
            
848
            $keep[] = $string;
849
            $keep[] = $withHyphen; 
850
            $keep[] = str_replace(array('-BE', '-LE'), '', $string);
851
            $keep[] = str_replace(array('-BE', '-LE'), '', $withHyphen);
852
        }
853
        
854
        return in_array($encoding, $keep);
855
    }
856
    
857
   /**
858
    * Retrieves a list of all known unicode file encodings.
859
    * @return array
860
    */
861
    public static function getKnownUnicodeEncodings()
862
    {
863
        return array_keys(self::getUTFBOMs());
864
    }
865
    
866
   /**
867
    * Normalizes the slash style in a file or folder path,
868
    * by replacing any antislashes with forward slashes.
869
    * 
870
    * @param string $path
871
    * @return string
872
    */
873
    public static function normalizePath(string $path) : string
874
    {
875
        return str_replace(array('\\', '//'), array('/', '/'), $path);
876
    }
877
    
878
   /**
879
    * Saves the specified data to a file, JSON encoded.
880
    * 
881
    * @param mixed $data
882
    * @param string $file
883
    * @param bool $pretty
884
    * @throws FileHelper_Exception
885
    * 
886
    * @see FileHelper::ERROR_JSON_ENCODE_ERROR
887
    * @see FileHelper::ERROR_SAVE_FOLDER_NOT_WRITABLE
888
    * @see FileHelper::ERROR_SAVE_FILE_NOT_WRITABLE
889
    * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
890
    */
891
    public static function saveAsJSON($data, string $file, bool $pretty=false)
892
    {
893
        $options = null;
894
        if($pretty) {
895
            $options = JSON_PRETTY_PRINT;
896
        }
897
        
898
        $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

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