UploadedFile::getSize()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace HttpSoft\Message;
6
7
use InvalidArgumentException;
8
use Psr\Http\Message\StreamInterface;
9
use Psr\Http\Message\UploadedFileInterface;
10
use RuntimeException;
11
12
use function array_key_exists;
13
use function array_keys;
14
use function dirname;
15
use function fclose;
16
use function fopen;
17
use function fwrite;
18
use function gettype;
19
use function get_class;
20
use function implode;
21
use function is_dir;
22
use function is_object;
23
use function is_resource;
24
use function is_string;
25
use function is_writable;
26
use function move_uploaded_file;
27
use function rename;
28
use function sprintf;
29
use function strpos;
30
31
use const PHP_SAPI;
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
final class UploadedFile implements UploadedFileInterface
42
{
43
    /**
44
     * @const array
45
     * @link https://www.php.net/manual/en/features.file-upload.errors.php
46
     */
47
    private const ERRORS = [
48
        UPLOAD_ERR_OK => 'There is no error, the file uploaded with success.',
49
        UPLOAD_ERR_INI_SIZE => 'The uploaded file exceeds the upload_max_filesize directive in php.ini.',
50
        UPLOAD_ERR_FORM_SIZE => 'The uploaded file exceeds the MAX_FILE_SIZE directive'
51
            . ' that was specified in the HTML form.',
52
        UPLOAD_ERR_PARTIAL => 'The uploaded file was only partially uploaded.',
53
        UPLOAD_ERR_NO_FILE => 'No file was uploaded.',
54
        UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
55
        UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
56
        UPLOAD_ERR_EXTENSION => 'A PHP extension stopped the file upload.',
57
    ];
58
59
    /**
60
     * @var StreamInterface|null
61
     */
62
    private ?StreamInterface $stream = null;
63
64
    /**
65
     * @var string|null
66
     */
67
    private ?string $file = null;
68
69
    /**
70
     * @var int
71
     */
72
    private int $size;
73
74
    /**
75
     * @var int
76
     */
77
    private int $error;
78
79
    /**
80
     * @var string|null
81
     */
82
    private ?string $clientFilename;
83
84
    /**
85
     * @var string|null
86
     */
87
    private ?string $clientMediaType;
88
89
    /**
90
     * @var bool
91
     */
92
    private bool $isMoved = false;
93
94
    /**
95
     * @param StreamInterface|string|resource $streamOrFile The file to upload. Must be one of the following:
96
     * - an instance of `StreamInterface`;
97
     * - a string stream identifier (e.g., 'php://temp') or a file path;
98
     * - a valid stream resource.
99
     * @param int $size The size of the uploaded file.
100
     * @param int $error The error code.
101
     * @param string|null $clientFilename Client filename.
102
     * @param string|null $clientMediaType Client media type.
103
     * @psalm-suppress DocblockTypeContradiction
104
     * @psalm-suppress RedundantConditionGivenDocblockType
105
     */
106 59
    public function __construct(
107
        $streamOrFile,
108
        int $size,
109
        int $error,
110
        ?string $clientFilename = null,
111
        ?string $clientMediaType = null
112
    ) {
113 59
        if (!array_key_exists($error, self::ERRORS)) {
114 4
            throw new InvalidArgumentException(sprintf(
115 4
                '"%s" is not valid error status for "UploadedFile". It must be one of "UPLOAD_ERR_*" constants:  "%s".',
116 4
                $error,
117 4
                implode('", "', array_keys(self::ERRORS))
118 4
            ));
119
        }
120
121 55
        $this->size = $size;
122 55
        $this->error = $error;
123 55
        $this->clientFilename = $clientFilename;
124 55
        $this->clientMediaType = $clientMediaType;
125
126 55
        if ($error !== UPLOAD_ERR_OK) {
127 14
            return;
128
        }
129
130 41
        if (is_string($streamOrFile)) {
131 3
            $this->file = $streamOrFile;
132 3
            return;
133
        }
134
135 39
        if (is_resource($streamOrFile)) {
136 1
            $this->stream = new Stream($streamOrFile);
137 1
            return;
138
        }
139
140 39
        if ($streamOrFile instanceof StreamInterface) {
0 ignored issues
show
introduced by
$streamOrFile is always a sub-type of Psr\Http\Message\StreamInterface.
Loading history...
141 30
            $this->stream = $streamOrFile;
142 30
            return;
143
        }
144
145 9
        throw new InvalidArgumentException(sprintf(
146 9
            '"%s" is not valid stream or file provided for "UploadedFile".',
147 9
            (is_object($streamOrFile) ? get_class($streamOrFile) : gettype($streamOrFile))
148 9
        ));
149
    }
150
151
    /**
152
     * {@inheritdoc}
153
     *
154
     * @psalm-suppress PossiblyNullArgument
155
     */
156 14
    public function getStream(): StreamInterface
157
    {
158 14
        if ($this->error !== UPLOAD_ERR_OK) {
159 7
            throw new RuntimeException(self::ERRORS[$this->error]);
160
        }
161
162 7
        if ($this->isMoved) {
163 2
            throw new RuntimeException('The stream is not available because it has been moved.');
164
        }
165
166 5
        if ($this->stream === null) {
167 2
            $this->stream = new Stream($this->file, 'r+');
168
        }
169
170 5
        return $this->stream;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->stream could return the type null which is incompatible with the type-hinted return Psr\Http\Message\StreamInterface. Consider adding an additional type-check to rule them out.
Loading history...
171
    }
172
173
    /**
174
     * {@inheritdoc}
175
     *
176
     * @psalm-suppress NoValue, RedundantCondition, TypeDoesNotContainType
177
     */
178 25
    public function moveTo($targetPath): void
179
    {
180 25
        if ($this->error !== UPLOAD_ERR_OK) {
181 7
            throw new RuntimeException(self::ERRORS[$this->error]);
182
        }
183
184 18
        if ($this->isMoved) {
185 2
            throw new RuntimeException('The file cannot be moved because it has already been moved.');
186
        }
187
188 18
        if (!is_string($targetPath)) {
189 9
            throw new InvalidArgumentException(sprintf(
190 9
                '"%s" is not valid target path for move. It must be a string type.',
191 9
                (is_object($targetPath) ? get_class($targetPath) : gettype($targetPath))
192 9
            ));
193
        }
194
195 9
        if (empty($targetPath)) {
196 1
            throw new InvalidArgumentException('Target path is not valid for move. It must be a non-empty string.');
197
        }
198
199 8
        $targetDirectory = dirname($targetPath);
200
201 8
        if (!is_dir($targetDirectory) || !is_writable($targetDirectory)) {
202 1
            throw new RuntimeException(sprintf(
203 1
                'The target directory "%s" does not exist or is not writable.',
204 1
                $targetDirectory
205 1
            ));
206
        }
207
208 7
        $this->moveOrWriteFile($targetPath);
209 7
        $this->isMoved = true;
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215 4
    public function getSize(): ?int
216
    {
217 4
        return $this->size;
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223 3
    public function getError(): int
224
    {
225 3
        return $this->error;
226
    }
227
228
    /**
229
     * {@inheritdoc}
230
     */
231 4
    public function getClientFilename(): ?string
232
    {
233 4
        return $this->clientFilename;
234
    }
235
236
    /**
237
     * {@inheritdoc}
238
     */
239 4
    public function getClientMediaType(): ?string
240
    {
241 4
        return $this->clientMediaType;
242
    }
243
244
    /**
245
     * Moves if used in an SAPI environment where $_FILES is populated, when writing
246
     * files via is_uploaded_file() and move_uploaded_file() or writes If SAPI is not used.
247
     *
248
     * @param string $targetPath
249
     * @psalm-suppress PossiblyNullReference
250
     */
251 7
    private function moveOrWriteFile(string $targetPath): void
252
    {
253 7
        if ($this->file !== null) {
254
            $isCliEnv = (!PHP_SAPI || strpos(PHP_SAPI, 'cli') === 0 || strpos(PHP_SAPI, 'phpdbg') === 0);
255
256
            if (!($isCliEnv ? rename($this->file, $targetPath) : move_uploaded_file($this->file, $targetPath))) {
257
                throw new RuntimeException(sprintf('Uploaded file could not be moved to "%s".', $targetPath));
258
            }
259
260
            return;
261
        }
262
263 7
        if (!$file = fopen($targetPath, 'wb+')) {
264
            throw new RuntimeException(sprintf('Unable to write to "%s".', $targetPath));
265
        }
266
267 7
        $this->stream->rewind();
0 ignored issues
show
Bug introduced by
The method rewind() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

267
        $this->stream->/** @scrutinizer ignore-call */ 
268
                       rewind();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
268
269 7
        while (!$this->stream->eof()) {
270 7
            fwrite($file, $this->stream->read(512000));
271
        }
272
273 7
        fclose($file);
274
    }
275
}
276