Test Failed
Push — master ( ca159c...8f4401 )
by Sebastian
03:00
created

FileInfo::createInstance()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 20
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 10
c 0
b 0
f 0
dl 0
loc 20
rs 9.9332
cc 3
nc 3
nop 1
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
    public function getExtension(bool $lowercase=true) : string
148
    {
149
        $ext = (string)pathinfo($this->path, PATHINFO_EXTENSION);
150
151
        if($lowercase)
152
        {
153
            $ext = mb_strtolower($ext);
154
        }
155
156
        return $ext;
157
    }
158
159
    public function getFolderPath() : string
160
    {
161
        return dirname($this->path);
162
    }
163
164
    /**
165
     * @return $this
166
     *
167
     * @throws FileHelper_Exception
168
     * @see FileHelper::ERROR_CANNOT_DELETE_FILE
169
     */
170
    public function delete() : FileInfo
171
    {
172
        if(!$this->exists())
173
        {
174
            return $this;
175
        }
176
177
        if(unlink($this->path))
178
        {
179
            return $this;
180
        }
181
182
        throw new FileHelper_Exception(
183
            sprintf(
184
                'Cannot delete file [%s].',
185
                $this->getName()
186
            ),
187
            sprintf(
188
                'The file [%s] cannot be deleted.',
189
                $this->getPath()
190
            ),
191
            FileHelper::ERROR_CANNOT_DELETE_FILE
192
        );
193
    }
194
195
    /**
196
     * @param string|PathInfoInterface|SplFileInfo $targetPath
197
     * @return FileInfo
198
     * @throws FileHelper_Exception
199
     */
200
    public function copyTo($targetPath) : FileInfo
201
    {
202
        $target = $this->checkCopyPrerequisites($targetPath);
203
204
        if(copy($this->path, (string)$target))
205
        {
206
            return $target;
207
        }
208
209
        throw new FileHelper_Exception(
210
            sprintf(
211
                'Cannot copy file [%s].',
212
                $this->getName()
213
            ),
214
            sprintf(
215
                'The file [%s] could not be copied from [%s] to [%s].',
216
                $this->getName(),
217
                $this->path,
218
                $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

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