FileInfo::copyTo()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 21
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 13
c 1
b 0
f 0
dl 0
loc 21
rs 9.8333
cc 2
nc 2
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
use function AppUtils\parseVariable;
22
23
/**
24
 * Specialized class used to access information on a file path,
25
 * and do file-related operations: reading contents, deleting
26
 * or copying and the like.
27
 *
28
 * Create an instance with {@see FileInfo::factory()}.
29
 *
30
 * Some specialized file type classes exist:
31
 *
32
 * - {@see JSONFile}
33
 * - {@see SerializedFile}
34
 * - {@see PHPFile}
35
 *
36
 * @package Application Utils
37
 * @subpackage FileHelper
38
 * @author Sebastian Mordziol <[email protected]>
39
 */
40
class FileInfo extends AbstractPathInfo
41
{
42
    public const ERROR_INVALID_INSTANCE_CREATED = 115601;
43
44
    /**
45
     * @var array<string,FileInfo>
46
     */
47
    protected static $infoCache = array();
48
49
    /**
50
     * @param string|PathInfoInterface|SplFileInfo $path
51
     * @return FileInfo
52
     * @throws FileHelper_Exception
53
     */
54
    public static function factory($path) : FileInfo
55
    {
56
        return self::createInstance($path);
57
    }
58
59
    /**
60
     * @param string|PathInfoInterface|SplFileInfo $path
61
     * @return FileInfo
62
     * @throws FileHelper_Exception
63
     */
64
    protected static function createInstance($path) : FileInfo
65
    {
66
        $pathString = AbstractPathInfo::type2string($path);
67
        $endingChar = $pathString[strlen($pathString) - 1];
68
69
        if(empty($path)) {
70
            throw new FileHelper_Exception(
71
                'Invalid',
72
                '',
73
                FileHelper::ERROR_PATH_INVALID
74
            );
75
        }
76
77
        if($path instanceof FolderInfo || $endingChar === '/' || $endingChar === '\\')
78
        {
79
            throw new FileHelper_Exception(
80
                'Cannot use a folder as a file',
81
                sprintf(
82
                    'This looks like a folder path: [%s].',
83
                    $pathString
84
                ),
85
                FileHelper::ERROR_PATH_IS_NOT_A_FILE
86
            );
87
        }
88
89
        $key = $pathString.';'.static::class;
90
91
        if(!isset(self::$infoCache[$key]))
92
        {
93
            $class = static::class;
94
            $instance = new $class($pathString);
95
96
            if(!$instance instanceof self) {
0 ignored issues
show
introduced by
$instance is always a sub-type of self.
Loading history...
97
                throw new FileHelper_Exception(
98
                    'Invalid class created',
99
                    sprintf(
100
                        'Expected: [%s]'.PHP_EOL.
101
                        'Created: [%s]',
102
                        self::class,
103
                        parseVariable($instance)->enableType()->toString()
104
                    ),
105
                    self::ERROR_INVALID_INSTANCE_CREATED
106
                );
107
            }
108
109
            self::$infoCache[$key] = $instance;
110
        }
111
112
        return self::$infoCache[$key];
113
    }
114
115
    /**
116
     * Clears the file cache that keeps track of any files
117
     * created via {@see FileInfo::factory()} for performance
118
     * reasons.
119
     *
120
     * @return void
121
     */
122
    public static function clearCache() : void
123
    {
124
        self::$infoCache = array();
125
    }
126
127
    public static function is_file(string $path) : bool
128
    {
129
        $path = trim($path);
130
131
        if(empty($path) || FolderInfo::is_dir($path))
132
        {
133
            return false;
134
        }
135
136
        return is_file($path) || pathinfo($path, PATHINFO_EXTENSION) !== '';
137
    }
138
139
    public function removeExtension(bool $keepPath=false) : string
140
    {
141
        if(!$keepPath)
142
        {
143
            return (string)pathinfo($this->getName(), PATHINFO_FILENAME);
144
        }
145
146
        $parts = explode('/', $this->path);
147
148
        $file = pathinfo(array_pop($parts), PATHINFO_FILENAME);
149
150
        $parts[] = $file;
151
152
        return implode('/', $parts);
153
    }
154
155
    /**
156
     * Gets the file name without extension.
157
     * @return string
158
     *
159
     * @see FileInfo::removeExtension()
160
     */
161
    public function getBaseName() : string
162
    {
163
        return $this->removeExtension();
164
    }
165
166
    public function getExtension(bool $lowercase=true) : string
167
    {
168
        $ext = (string)pathinfo($this->path, PATHINFO_EXTENSION);
169
170
        if($lowercase)
171
        {
172
            $ext = mb_strtolower($ext);
173
        }
174
175
        return $ext;
176
    }
177
178
    public function getFolderPath() : string
179
    {
180
        return dirname($this->path);
181
    }
182
183
    /**
184
     * @return $this
185
     *
186
     * @throws FileHelper_Exception
187
     * @see FileHelper::ERROR_CANNOT_DELETE_FILE
188
     */
189
    public function delete() : FileInfo
190
    {
191
        if(!$this->exists())
192
        {
193
            return $this;
194
        }
195
196
        if(unlink($this->path))
197
        {
198
            return $this;
199
        }
200
201
        throw new FileHelper_Exception(
202
            sprintf(
203
                'Cannot delete file [%s].',
204
                $this->getName()
205
            ),
206
            sprintf(
207
                'The file [%s] cannot be deleted.',
208
                $this->getPath()
209
            ),
210
            FileHelper::ERROR_CANNOT_DELETE_FILE
211
        );
212
    }
213
214
    /**
215
     * @param string|PathInfoInterface|SplFileInfo $targetPath
216
     * @return FileInfo
217
     * @throws FileHelper_Exception
218
     */
219
    public function copyTo($targetPath) : FileInfo
220
    {
221
        $target = $this->checkCopyPrerequisites($targetPath);
222
223
        if(copy($this->path, (string)$target))
224
        {
225
            return $target;
226
        }
227
228
        throw new FileHelper_Exception(
229
            sprintf(
230
                'Cannot copy file [%s].',
231
                $this->getName()
232
            ),
233
            sprintf(
234
                'The file [%s] could not be copied from [%s] to [%s].',
235
                $this->getName(),
236
                $this->path,
237
                $targetPath
238
            ),
239
            FileHelper::ERROR_CANNOT_COPY_FILE
240
        );
241
    }
242
243
    /**
244
     * @param string|PathInfoInterface|SplFileInfo $targetPath
245
     * @return FileInfo
246
     *
247
     * @throws FileHelper_Exception
248
     * @see FileHelper::ERROR_SOURCE_FILE_NOT_FOUND
249
     * @see FileHelper::ERROR_SOURCE_FILE_NOT_READABLE
250
     * @see FileHelper::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE
251
     */
252
    private function checkCopyPrerequisites($targetPath) : FileInfo
253
    {
254
        $this->requireExists(FileHelper::ERROR_SOURCE_FILE_NOT_FOUND);
255
        $this->requireReadable(FileHelper::ERROR_SOURCE_FILE_NOT_READABLE);
256
257
        $target = FileHelper::getPathInfo($targetPath);
258
259
        // It's a file? Then we can use it as-is.
260
        if($target instanceof self) {
261
            return $target
262
                ->requireIsFile()
263
                ->createFolder();
264
        }
265
266
        // The target is a path that can not be recognized as a file,
267
        // but is not a folder: very likely a file without extension.
268
        // In this case we create an empty file to be able to return
269
        // a FileInfo instance.
270
        if($target instanceof IndeterminatePath)
271
        {
272
            return $target->convertToFile();
273
        }
274
275
        throw new FileHelper_Exception(
276
            'Cannot copy a file to a folder.',
277
            sprintf(
278
                'Tried to copy file [%s] to folder [%s].',
279
                $this,
280
                $target
281
            ),
282
            FileHelper::ERROR_CANNOT_COPY_FILE_TO_FOLDER
283
        );
284
    }
285
286
    /**
287
     * @var LineReader|NULL
288
     */
289
    private ?LineReader $lineReader = null;
290
291
    /**
292
     * Gets an instance of the line reader, which can
293
     * read contents of the file, line by line.
294
     *
295
     * @return LineReader
296
     */
297
    public function getLineReader() : LineReader
298
    {
299
        if($this->lineReader === null)
300
        {
301
            $this->lineReader = new LineReader($this);
302
        }
303
304
        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...
305
    }
306
307
    /**
308
     * @return string
309
     * @throws FileHelper_Exception
310
     * @see FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
311
     */
312
    public function getContents() : string
313
    {
314
        $this->requireExists();
315
316
        $result = file_get_contents($this->getPath());
317
318
        if($result !== false) {
319
            return $result;
320
        }
321
322
        throw new FileHelper_Exception(
323
            sprintf('Cannot read contents of file [%s].', $this->getName()),
324
            sprintf(
325
                'Tried opening file for reading at: [%s].',
326
                $this->getPath()
327
            ),
328
            FileHelper::ERROR_CANNOT_READ_FILE_CONTENTS
329
        );
330
    }
331
332
    /**
333
     * @param string $content
334
     * @return $this
335
     * @throws FileHelper_Exception
336
     * @see FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
337
     */
338
    public function putContents(string $content) : FileInfo
339
    {
340
        if($this->exists())
341
        {
342
            $this->requireWritable();
343
        }
344
        else
345
        {
346
            FolderInfo::factory(dirname($this->path))
347
                ->create()
348
                ->requireWritable();
349
        }
350
351
        if(file_put_contents($this->path, $content) !== false)
352
        {
353
            return $this;
354
        }
355
356
        throw new FileHelper_Exception(
357
            sprintf('Cannot save file: writing content to the file [%s] failed.', $this->getName()),
358
            sprintf(
359
                'Tried saving content to file in path [%s].',
360
                $this->getPath()
361
            ),
362
            FileHelper::ERROR_SAVE_FILE_WRITE_FAILED
363
        );
364
    }
365
366
    public function getDownloader() : FileSender
367
    {
368
        return new FileSender($this);
369
    }
370
371
    /**
372
     * Attempts to create the folder of the file, if it
373
     * does not exist yet. Use this with files that do
374
     * not exist in the file system yet.
375
     *
376
     * @return $this
377
     * @throws FileHelper_Exception
378
     */
379
    private function createFolder() : FileInfo
380
    {
381
        if(!$this->exists())
382
        {
383
            FolderInfo::factory($this->getFolderPath())
384
                ->create()
385
                ->requireWritable(FileHelper::ERROR_TARGET_COPY_FOLDER_NOT_WRITABLE);
386
        }
387
388
        return $this;
389
    }
390
391
    /**
392
     * Detects the end of line style used in the target file, if any.
393
     * Can be used with large files, because it only reads part of it.
394
     *
395
     * @return NULL|ConvertHelper_EOL The end of line character information, or NULL if none is found.
396
     * @throws FileHelper_Exception
397
     */
398
    public function detectEOLCharacter() : ?ConvertHelper_EOL
399
    {
400
        // 20 lines is enough to get a good picture of the newline style in the file.
401
        $string = implode('', $this->getLineReader()->getLines(20));
402
403
        return ConvertHelper::detectEOLCharacter($string);
404
    }
405
406
    public function countLines() : int
407
    {
408
        return $this->getLineReader()->countLines();
409
    }
410
411
    public function getLine(int $lineNumber) : ?string
412
    {
413
        return $this->getLineReader()->getLine($lineNumber);
414
    }
415
}
416