Passed
Push — master ( e41252...69cd60 )
by Evgeniy
01:59
created

UploadedFile::__construct()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 42
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7.2944

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 7
eloc 23
c 1
b 0
f 0
nc 6
nop 5
dl 0
loc 42
ccs 18
cts 22
cp 0.8182
crap 7.2944
rs 8.6186
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
96
     * @param int $size
97
     * @param int $error
98
     * @param string|null $clientFilename
99
     * @param string|null $clientMediaType
100
     * @psalm-suppress RedundantConditionGivenDocblockType
101
     */
102 37
    public function __construct(
103
        $streamOrFile,
104
        int $size,
105
        int $error,
106
        string $clientFilename = null,
107
        string $clientMediaType = null
108
    ) {
109 37
        if (!array_key_exists($error, self::ERRORS)) {
110 4
            throw new InvalidArgumentException(sprintf(
111
                '`%s` is not valid error status for `UploadedFile`. Must be number from `UPLOAD_ERR_*` constant (%s)',
112 4
                $error,
113 4
                implode(', ', array_keys(self::ERRORS))
114
            ));
115
        }
116
117 33
        $this->size = $size;
118 33
        $this->error = $error;
119 33
        $this->clientFilename = $clientFilename;
120 33
        $this->clientMediaType = $clientMediaType;
121
122 33
        if ($error !== UPLOAD_ERR_OK) {
123 14
            return;
124
        }
125
126 19
        if (is_string($streamOrFile)) {
127 2
            $this->file = $streamOrFile;
128 2
            return;
129
        }
130
131 18
        if (is_resource($streamOrFile)) {
132
            $this->stream = new Stream($streamOrFile);
133
            return;
134
        }
135
136 18
        if ($streamOrFile instanceof StreamInterface) {
0 ignored issues
show
introduced by
$streamOrFile is always a sub-type of Psr\Http\Message\StreamInterface.
Loading history...
137 18
            $this->stream = $streamOrFile;
138 18
            return;
139
        }
140
141
        throw new InvalidArgumentException(sprintf(
142
            '`%s` is not valid stream or file provided for `UploadedFile`.',
143
            (is_object($streamOrFile) ? get_class($streamOrFile) : gettype($streamOrFile))
144
        ));
145
    }
146
147
    /**
148
     * {@inheritdoc}
149
     *
150
     * @psalm-suppress PossiblyNullArgument
151
     */
152 11
    public function getStream(): StreamInterface
153
    {
154 11
        if ($this->error !== UPLOAD_ERR_OK) {
155 7
            throw new RuntimeException(self::ERRORS[$this->error]);
156
        }
157
158 4
        if ($this->isMoved) {
159 1
            throw new RuntimeException('The stream is not available because it has been moved.');
160
        }
161
162 3
        if ($this->stream === null) {
163 1
            $this->stream = new Stream($this->file);
164
        }
165
166 3
        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...
167
    }
168
169
    /**
170
     * {@inheritdoc}
171
     *
172
     * @psalm-suppress DocblockTypeContradiction
173
     */
174 19
    public function moveTo($targetPath): void
175
    {
176 19
        if ($this->error !== UPLOAD_ERR_OK) {
177 7
            throw new RuntimeException(self::ERRORS[$this->error]);
178
        }
179
180 12
        if ($this->isMoved) {
181 1
            throw new RuntimeException('The file cannot be moved because it has already been moved.');
182
        }
183
184 12
        if (!is_string($targetPath)) {
185 9
            throw new InvalidArgumentException(sprintf(
186
                '`%s` is not valid target path for move. Must be a string type.',
187 9
                (is_object($targetPath) ? get_class($targetPath) : gettype($targetPath))
188
            ));
189
        }
190
191 3
        if (empty($targetPath)) {
192
            throw new InvalidArgumentException('Target path is not valid for move. Must be a non-empty string.');
193
        }
194
195 3
        $targetDirectory = dirname($targetPath);
196
197 3
        if (!is_dir($targetDirectory) || !is_writable($targetDirectory)) {
198
            throw new RuntimeException(sprintf(
199
                'The target directory `%s` does not exists or is not writable.',
200
                $targetDirectory
201
            ));
202
        }
203
204 3
        $this->moveOrWriteFile($targetPath);
205 3
        $this->isMoved = true;
206 3
    }
207
208
    /**
209
     * {@inheritdoc}
210
     */
211 3
    public function getSize(): ?int
212
    {
213 3
        return $this->size;
214
    }
215
216
    /**
217
     * {@inheritdoc}
218
     */
219 2
    public function getError(): int
220
    {
221 2
        return $this->error;
222
    }
223
224
    /**
225
     * {@inheritdoc}
226
     */
227 3
    public function getClientFilename(): ?string
228
    {
229 3
        return $this->clientFilename;
230
    }
231
232
    /**
233
     * {@inheritdoc}
234
     */
235 3
    public function getClientMediaType(): ?string
236
    {
237 3
        return $this->clientMediaType;
238
    }
239
240
    /**
241
     * Moves if used in an SAPI environment where $_FILES is populated, when writing
242
     * files via is_uploaded_file() and move_uploaded_file() or writes If SAPI is not used.
243
     *
244
     * @param string $targetPath
245
     * @psalm-suppress PossiblyNullReference
246
     */
247 3
    private function moveOrWriteFile(string $targetPath): void
248
    {
249 3
        if ($this->file) {
250
            $isCliEnv = (!PHP_SAPI || strpos(PHP_SAPI, 'cli') === 0 || strpos(PHP_SAPI, 'phpdbg') === 0);
251
252
            if (!($isCliEnv ? rename($this->file, $targetPath) : move_uploaded_file($this->file, $targetPath))) {
253
                throw new RuntimeException(sprintf('Uploaded file could not be moved to `%s`', $targetPath));
254
            }
255
256
            return;
257
        }
258
259 3
        if (!$file = fopen($targetPath, 'wb+')) {
260
            throw new RuntimeException(sprintf('Unable to write to designated path (%s).', $targetPath));
261
        }
262
263 3
        $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

263
        $this->stream->/** @scrutinizer ignore-call */ 
264
                       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...
264
265 3
        while (!$this->stream->eof()) {
266 3
            fwrite($file, $this->stream->read(512000));
267
        }
268
269 3
        fclose($file);
270 3
    }
271
}
272