Passed
Push — master ( 991457...946f98 )
by Anatoly
02:57 queued 15s
created

UploadedFile   A

Complexity

Total Complexity 20

Size/Duplication

Total Lines 136
Duplicated Lines 0 %

Test Coverage

Coverage 89.36%

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 56
c 2
b 0
f 0
dl 0
loc 136
ccs 42
cts 47
cp 0.8936
rs 10
wmc 20

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
A getClientMediaType() 0 3 1
A getClientFilename() 0 3 1
A getSize() 0 3 1
A getError() 0 3 1
A getStream() 0 15 4
B moveTo() 0 40 11
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message;
13
14
use Psr\Http\Message\StreamInterface;
15
use Psr\Http\Message\UploadedFileInterface;
16
use Sunrise\Http\Message\Exception\RuntimeException;
17
use Throwable;
18
use TypeError;
19
20
use function dirname;
21
use function gettype;
22
use function is_dir;
23
use function is_file;
24
use function is_readable;
25
use function is_string;
26
use function is_uploaded_file;
27
use function is_writable;
28
use function move_uploaded_file;
29
use function rename;
30
use function sprintf;
31
32
use const UPLOAD_ERR_OK;
33
use const UPLOAD_ERR_INI_SIZE;
34
use const UPLOAD_ERR_FORM_SIZE;
35
use const UPLOAD_ERR_PARTIAL;
36
use const UPLOAD_ERR_NO_FILE;
37
use const UPLOAD_ERR_NO_TMP_DIR;
38
use const UPLOAD_ERR_CANT_WRITE;
39
use const UPLOAD_ERR_EXTENSION;
40
41
class UploadedFile implements UploadedFileInterface
42
{
43
    /**
44
     * @link https://www.php.net/manual/en/features.file-upload.errors.php
45
     *
46
     * @var array<int, non-empty-string>
47
     */
48
    public const UPLOAD_ERRORS = [
49
        UPLOAD_ERR_OK         => 'No error',
50
        UPLOAD_ERR_INI_SIZE   => 'Uploaded file exceeds the upload_max_filesize directive in the php.ini',
51
        UPLOAD_ERR_FORM_SIZE  => 'Uploaded file exceeds the MAX_FILE_SIZE directive in the HTML form',
52
        UPLOAD_ERR_PARTIAL    => 'Uploaded file was only partially uploaded',
53
        UPLOAD_ERR_NO_FILE    => 'No file was uploaded',
54
        UPLOAD_ERR_NO_TMP_DIR => 'Missing temporary directory',
55
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk',
56
        UPLOAD_ERR_EXTENSION  => 'File upload was stopped by a PHP extension',
57
    ];
58
59
    private ?StreamInterface $stream;
60
    private ?int $size;
61
    private int $errorCode;
62
    private string $errorMessage;
63
    private ?string $clientFilename;
64
    private ?string $clientMediaType;
65
    private bool $isMoved = false;
66
67 40
    public function __construct(
68
        ?StreamInterface $stream,
69
        ?int $size = null,
70
        int $error = UPLOAD_ERR_OK,
71
        ?string $clientFilename = null,
72
        ?string $clientMediaType = null
73
    ) {
74 40
        $this->stream = $stream;
75 40
        $this->size = $size;
76 40
        $this->errorCode = $error;
77 40
        $this->errorMessage = self::UPLOAD_ERRORS[$error] ?? 'Unknown file upload error';
78 40
        $this->clientFilename = $clientFilename;
79 40
        $this->clientMediaType = $clientMediaType;
80
    }
81
82
    /**
83
     * @inheritDoc
84
     */
85 34
    public function getStream(): StreamInterface
86
    {
87 34
        if ($this->isMoved) {
88 4
            throw new RuntimeException('Uploaded file was moved');
89
        }
90
91 34
        if ($this->errorCode !== UPLOAD_ERR_OK) {
92 16
            throw new RuntimeException($this->errorMessage, $this->errorCode);
93
        }
94
95 18
        if ($this->stream === null) {
96 2
            throw new RuntimeException('Uploaded file has no stream');
97
        }
98
99 16
        return $this->stream;
100
    }
101
102
    /**
103
     * @inheritDoc
104
     */
105 19
    public function moveTo($targetPath): void
106
    {
107
        /** @psalm-suppress TypeDoesNotContainType */
108 19
        if (!is_string($targetPath)) {
109 1
            throw new TypeError(sprintf(
110 1
                'Argument #1 ($targetPath) must be of type string, %s given',
111 1
                gettype($targetPath),
112 1
            ));
113
        }
114
115 18
        $sourceStream = $this->getStream();
116
117 9
        $sourcePath = $sourceStream->getMetadata('uri');
118 9
        if (!is_string($sourcePath) || !is_file($sourcePath) || !is_readable($sourcePath)) {
119 1
            throw new RuntimeException('Uploaded file does not exist or is not readable');
120
        }
121
122 8
        $sourceDirname = dirname($sourcePath);
123 8
        if (!is_writable($sourceDirname)) {
124
            throw new RuntimeException('To move the uploaded file, the source directory must be writable');
125
        }
126
127 8
        $targetDirname = dirname($targetPath);
128 8
        if (!is_dir($targetDirname) || !is_writable($targetDirname)) {
129
            throw new RuntimeException('To move the uploaded file, the target directory must exist and be writable');
130
        }
131
132
        try {
133 8
            $this->isMoved = is_uploaded_file($sourcePath)
134
                ? move_uploaded_file($sourcePath, $targetPath)
135 8
                : rename($sourcePath, $targetPath);
136
        } catch (Throwable $e) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
137
        }
138
139 8
        if (!$this->isMoved) {
140
            throw new RuntimeException('Failed to move the uploaded file');
141
        }
142
143 8
        $sourceStream->close();
144 8
        $this->stream = null;
145
    }
146
147
    /**
148
     * @inheritDoc
149
     */
150 6
    public function getSize(): ?int
151
    {
152 6
        return $this->size;
153
    }
154
155
    /**
156
     * @inheritDoc
157
     */
158 7
    public function getError(): int
159
    {
160 7
        return $this->errorCode;
161
    }
162
163
    /**
164
     * @inheritDoc
165
     */
166 7
    public function getClientFilename(): ?string
167
    {
168 7
        return $this->clientFilename;
169
    }
170
171
    /**
172
     * @inheritDoc
173
     */
174 7
    public function getClientMediaType(): ?string
175
    {
176 7
        return $this->clientMediaType;
177
    }
178
}
179