Passed
Push — master ( 589e16...f15a7f )
by Sebastian
08:28
created

FileInfo   A

Complexity

Total Complexity 36

Size/Duplication

Total Lines 343
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 36
eloc 106
c 2
b 0
f 0
dl 0
loc 343
rs 9.52

20 Methods

Rating   Name   Duplication   Size   Complexity  
A is_file() 0 10 3
A createInstance() 0 20 3
A removeExtension() 0 14 2
A __construct() 0 10 2
A clearCache() 0 3 1
A factory() 0 7 2
A getContents() 0 17 2
A delete() 0 22 3
A getLine() 0 3 1
A getExtension() 0 10 2
A getDownloader() 0 3 1
A countLines() 0 3 1
A copyTo() 0 21 2
A getBaseName() 0 3 1
A getFolderPath() 0 3 1
A checkCopyPrerequisites() 0 8 1
A putContents() 0 25 3
A getLineReader() 0 8 2
A detectEOLCharacter() 0 6 1
A createFolder() 0 10 2
1
<?php
2
/**
3
 * File containing the class {@see \AppUtils\FileHelper\FileInfo}.
4
 *
5
 * @package Application Utils
6
 * @subpackage FileHelper
7
 * @see \AppUtils\FileHelper\FileInfo
8
 */
9
10
declare(strict_types=1);
11
12
namespace AppUtils\FileHelper;
13
14
use AppUtils\ConvertHelper;
15
use AppUtils\ConvertHelper_EOL;
16
use AppUtils\FileHelper;
17
use AppUtils\FileHelper\FileInfo\FileSender;
18
use AppUtils\FileHelper\FileInfo\LineReader;
19
use AppUtils\FileHelper_Exception;
20
use SplFileInfo;
21
22
/**
23
 * Specialized class used to access information on a file path,
24
 * and do file-related operations: reading contents, deleting
25
 * or copying and the like.
26
 *
27
 * Create an instance with {@see FileInfo::factory()}.
28
 *
29
 * Some specialized file type classes exist:
30
 *
31
 * - {@see JSONFile}
32
 * - {@see SerializedFile}
33
 * - {@see PHPFile}
34
 *
35
 * @package Application Utils
36
 * @subpackage FileHelper
37
 * @author Sebastian Mordziol <[email protected]>
38
 */
39
class FileInfo extends AbstractPathInfo
40
{
41
    /**
42
     * @var array<string,FileInfo>
43
     */
44
    protected static $infoCache = array();
45
46
    /**
47
     * @param string|PathInfoInterface|SplFileInfo $path
48
     * @return FileInfo
49
     * @throws FileHelper_Exception
50
     */
51
    public static function factory($path) : FileInfo
52
    {
53
        if($path instanceof self) {
54
            return $path;
55
        }
56
57
        return self::createInstance($path);
58
    }
59
60
    /**
61
     * @param string|PathInfoInterface|SplFileInfo $path
62
     * @return FileInfo
63
     * @throws FileHelper_Exception
64
     */
65
    public static function createInstance($path) : FileInfo
66
    {
67
        $pathString = AbstractPathInfo::type2string($path);
68
        $key = $pathString.';'.static::class;
69
70
        if(!isset(self::$infoCache[$key]))
71
        {
72
            $class = static::class;
73
            $instance = new $class($pathString);
74
75
            if(!$instance instanceof self) {
0 ignored issues
show
introduced by
$instance is always a sub-type of self.
Loading history...
76
                throw new FileHelper_Exception(
77
                    'Invalid class'
78
                );
79
            }
80
81
            self::$infoCache[$key] = $instance;
82
        }
83
84
        return self::$infoCache[$key];
85
    }
86
87
    /**
88
     * Clears the file cache that keeps track of any files
89
     * created via {@see FileInfo::factory()} for performance
90
     * reasons.
91
     *
92
     * @return void
93
     */
94
    public static function clearCache() : void
95
    {
96
        self::$infoCache = array();
97
    }
98
99
    /**
100
     * @param string $path
101
     *
102
     * @throws FileHelper_Exception
103
     * @see FileHelper::ERROR_PATH_IS_NOT_A_FILE
104
     */
105
    public function __construct(string $path)
106
    {
107
        parent::__construct($path);
108
109
        if(!self::is_file($this->path))
110
        {
111
            throw new FileHelper_Exception(
112
                'Not a file path',
113
                sprintf('The path is not a file: [%s].', $this->path),
114
                FileHelper::ERROR_PATH_IS_NOT_A_FILE
115
            );
116
        }
117
    }
118
119
    public static function is_file(string $path) : bool
120
    {
121
        $path = trim($path);
122
123
        if(empty($path))
124
        {
125
            return false;
126
        }
127
128
        return is_file($path) || pathinfo($path, PATHINFO_EXTENSION) !== '';
129
    }
130
131
    public function removeExtension(bool $keepPath=false) : string
132
    {
133
        if(!$keepPath)
134
        {
135
            return (string)pathinfo($this->getName(), PATHINFO_FILENAME);
136
        }
137
138
        $parts = explode('/', $this->path);
139
140
        $file = pathinfo(array_pop($parts), PATHINFO_FILENAME);
141
142
        $parts[] = $file;
143
144
        return implode('/', $parts);
145
    }
146
147
    /**
148
     * Gets the file name without extension.
149
     * @return string
150
     *
151
     * @see FileInfo::removeExtension()
152
     */
153
    public function getBaseName() : string
154
    {
155
        return $this->removeExtension();
156
    }
157
158
    public function getExtension(bool $lowercase=true) : string
159
    {
160
        $ext = (string)pathinfo($this->path, PATHINFO_EXTENSION);
161
162
        if($lowercase)
163
        {
164
            $ext = mb_strtolower($ext);
165
        }
166
167
        return $ext;
168
    }
169
170
    public function getFolderPath() : string
171
    {
172
        return dirname($this->path);
173
    }
174
175
    /**
176
     * @return $this
177
     *
178
     * @throws FileHelper_Exception
179
     * @see FileHelper::ERROR_CANNOT_DELETE_FILE
180
     */
181
    public function delete() : FileInfo
182
    {
183
        if(!$this->exists())
184
        {
185
            return $this;
186
        }
187
188
        if(unlink($this->path))
189
        {
190
            return $this;
191
        }
192
193
        throw new FileHelper_Exception(
194
            sprintf(
195
                'Cannot delete file [%s].',
196
                $this->getName()
197
            ),
198
            sprintf(
199
                'The file [%s] cannot be deleted.',
200
                $this->getPath()
201
            ),
202
            FileHelper::ERROR_CANNOT_DELETE_FILE
203
        );
204
    }
205
206
    /**
207
     * @param string|PathInfoInterface|SplFileInfo $targetPath
208
     * @return FileInfo
209
     * @throws FileHelper_Exception
210
     */
211
    public function copyTo($targetPath) : FileInfo
212
    {
213
        $target = $this->checkCopyPrerequisites($targetPath);
214
215
        if(copy($this->path, (string)$target))
216
        {
217
            return $target;
218
        }
219
220
        throw new FileHelper_Exception(
221
            sprintf(
222
                'Cannot copy file [%s].',
223
                $this->getName()
224
            ),
225
            sprintf(
226
                'The file [%s] could not be copied from [%s] to [%s].',
227
                $this->getName(),
228
                $this->path,
229
                $targetPath
0 ignored issues
show
Bug introduced by
It seems like $targetPath can also be of type AppUtils\FileHelper\PathInfoInterface; however, parameter $values of sprintf() does only seem to accept double|integer|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

229
                /** @scrutinizer ignore-type */ $targetPath
Loading history...
230
            ),
231
            FileHelper::ERROR_CANNOT_COPY_FILE
232
        );
233
    }
234
235
    /**
236
     * @param string|PathInfoInterface|SplFileInfo $targetPath
237
     * @return FileInfo
238
     *
239
     * @throws FileHelper_Exception
240
     * @see FileHelper::ERROR_SOURCE_FILE_NOT_FOUND
241
     * @see FileHelper::ERROR_SOURCE_FILE_NOT_READABLE
242
     * @see FileHelper::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE
243
     */
244
    private function checkCopyPrerequisites($targetPath) : FileInfo
245
    {
246
        $this->requireExists(FileHelper::ERROR_SOURCE_FILE_NOT_FOUND);
247
        $this->requireReadable(FileHelper::ERROR_SOURCE_FILE_NOT_READABLE);
248
249
        return FileHelper::getPathInfo($targetPath)
250
            ->requireIsFile()
251
            ->createFolder();
252
    }
253
254
    /**
255
     * @var LineReader|NULL
256
     */
257
    private ?LineReader $lineReader = null;
258
259
    /**
260
     * Gets an instance of the line reader, which can
261
     * read contents of the file, line by line.
262
     *
263
     * @return LineReader
264
     */
265
    public function getLineReader() : LineReader
266
    {
267
        if($this->lineReader === null)
268
        {
269
            $this->lineReader = new LineReader($this);
270
        }
271
272
        return $this->lineReader;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->lineReader could return the type null which is incompatible with the type-hinted return AppUtils\FileHelper\FileInfo\LineReader. Consider adding an additional type-check to rule them out.
Loading history...
273
    }
274
275
    /**
276
     * @return string
277
     * @throws FileHelper_Exception
278
     * @see FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
279
     */
280
    public function getContents() : string
281
    {
282
        $this->requireExists();
283
284
        $result = file_get_contents($this->getPath());
285
286
        if($result !== false) {
287
            return $result;
288
        }
289
290
        throw new FileHelper_Exception(
291
            sprintf('Cannot read contents of file [%s].', $this->getName()),
292
            sprintf(
293
                'Tried opening file for reading at: [%s].',
294
                $this->getPath()
295
            ),
296
            FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
297
        );
298
    }
299
300
    /**
301
     * @param string $content
302
     * @return $this
303
     * @throws FileHelper_Exception
304
     * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
305
     */
306
    public function putContents(string $content) : FileInfo
307
    {
308
        if($this->exists())
309
        {
310
            $this->requireWritable();
311
        }
312
        else
313
        {
314
            FolderInfo::factory(dirname($this->path))
315
                ->create()
316
                ->requireWritable();
317
        }
318
319
        if(file_put_contents($this->path, $content) !== false)
320
        {
321
            return $this;
322
        }
323
324
        throw new FileHelper_Exception(
325
            sprintf('Cannot save file: writing content to the file [%s] failed.', $this->getName()),
326
            sprintf(
327
                'Tried saving content to file in path [%s].',
328
                $this->getPath()
329
            ),
330
            FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
331
        );
332
    }
333
334
    public function getDownloader() : FileSender
335
    {
336
        return new FileSender($this);
337
    }
338
339
    /**
340
     * Attempts to create the folder of the file, if it
341
     * does not exist yet. Use this with files that do
342
     * not exist in the file system yet.
343
     *
344
     * @return $this
345
     * @throws FileHelper_Exception
346
     */
347
    private function createFolder() : FileInfo
348
    {
349
        if(!$this->exists())
350
        {
351
            FolderInfo::factory($this->getFolderPath())
352
                ->create()
353
                ->requireWritable(FileHelper::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE);
354
        }
355
356
        return $this;
357
    }
358
359
    /**
360
     * Detects the end of line style used in the target file, if any.
361
     * Can be used with large files, because it only reads part of it.
362
     *
363
     * @return NULL|ConvertHelper_EOL The end of line character information, or NULL if none is found.
364
     * @throws FileHelper_Exception
365
     */
366
    public function detectEOLCharacter() : ?ConvertHelper_EOL
367
    {
368
        // 20 lines is enough to get a good picture of the newline style in the file.
369
        $string = implode('', $this->getLineReader()->getLines(20));
370
371
        return ConvertHelper::detectEOLCharacter($string);
372
    }
373
374
    public function countLines() : int
375
    {
376
        return $this->getLineReader()->countLines();
377
    }
378
379
    public function getLine(int $lineNumber) : ?string
380
    {
381
        return $this->getLineReader()->getLine($lineNumber);
382
    }
383
}
384