Passed
Push — develop ( ea363f...2e3b8b )
by nguereza
02:24
created

UploadedFile   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 258
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 96
c 1
b 0
f 0
dl 0
loc 258
rs 8.96
wmc 43

11 Methods

Rating   Name   Duplication   Size   Complexity  
A getClientFilename() 0 3 1
A filterTargetPath() 0 6 2
A createFromGlobals() 0 3 1
B moveTo() 0 37 10
A getClientMediaType() 0 3 1
A filterError() 0 18 2
C normalize() 0 40 16
A getError() 0 3 1
A getStream() 0 11 4
A __construct() 0 21 4
A getSize() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like UploadedFile often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use UploadedFile, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * Platine HTTP
5
 *
6
 * Platine HTTP Message is the implementation of PSR 7
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine HTTP
11
 * Copyright (c) 2019 Dion Chaika
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file UploadedFile.php
34
 *
35
 *  The UploadedFile class that represent the data for file upload
36
 *
37
 *  @package    Platine\Http
38
 *  @author Platine Developers Team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Http;
49
50
use InvalidArgumentException;
51
use RuntimeException;
52
53
class UploadedFile implements UploadedFileInterface
54
{
55
    /**
56
     * The uploaded file name
57
     * @var string
58
     */
59
    protected ?string $filename = null;
60
61
    /**
62
     * Whether the uploaded file is moved
63
     * @var bool
64
     */
65
    protected bool $moved = false;
66
67
    /**
68
     * The uploaded file stream
69
     * @var StreamInterface
70
     */
71
    protected ?StreamInterface $stream = null;
72
73
    /**
74
     *  The uploaded file size
75
     * @var int|null
76
     */
77
    protected ?int $size;
78
79
    /**
80
     *  The uploaded file error
81
     * @var int
82
     */
83
    protected int $error = \UPLOAD_ERR_OK;
84
85
    /**
86
     * The uploaded file client name
87
     * @var string|null
88
     */
89
    protected ?string $clientFilename;
90
91
    /**
92
     * The uploaded file client media type
93
     * @var string|null
94
     */
95
    protected ?string $clientMediaType;
96
97
    /**
98
     * Create new uploaded file instance
99
     *
100
     * @param string|StreamInterface $filenameOrStream the filename or stream
101
     * @param int|null $size the upload file size
102
     * @param int $error the upload error code
103
     * @param string|null $clientFilename
104
     * @param string|null $clientMediaType
105
     */
106
    public function __construct(
107
        $filenameOrStream,
108
        ?int $size = null,
109
        int $error = \UPLOAD_ERR_OK,
110
        ?string $clientFilename = null,
111
        ?string $clientMediaType = null
112
    ) {
113
        if ($filenameOrStream instanceof StreamInterface) {
114
            if (!$filenameOrStream->isReadable()) {
115
                throw new InvalidArgumentException('Stream is not readable');
116
            }
117
            $this->stream = $filenameOrStream;
118
            $this->size = $size ? $size : $filenameOrStream->getSize();
119
        } else {
120
            $this->filename = $filenameOrStream;
121
            $this->size = $size;
122
        }
123
124
        $this->error = $this->filterError($error);
125
        $this->clientFilename = $clientFilename;
126
        $this->clientMediaType = $clientMediaType;
127
    }
128
129
    /**
130
    * Create uploaded file from global variable $_FILES
131
    * @return array<mixed>
132
    */
133
    public static function createFromGlobals(): array
134
    {
135
        return static::normalize($_FILES);
136
    }
137
138
    /**
139
     * {@inheritdoc}
140
     */
141
    public function getStream(): StreamInterface
142
    {
143
        if ($this->moved) {
144
            throw new RuntimeException('Stream is not avaliable! Uploaded file is moved');
145
        }
146
147
        if ($this->stream === null && $this->filename !== null) {
148
            $this->stream = new Stream($this->filename);
149
        }
150
151
        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 Platine\Http\StreamInterface. Consider adding an additional type-check to rule them out.
Loading history...
152
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157
    public function moveTo(string $targetPath): void
158
    {
159
        if ($this->moved) {
160
            throw new RuntimeException('Uploaded file is already moved');
161
        }
162
163
        $cleanTargetPath = $this->filterTargetPath($targetPath);
164
        if ($this->filename !== null) {
165
            if (php_sapi_name() === 'cli') {
166
                if (rename($this->filename, $cleanTargetPath) === false) {
167
                    throw new RuntimeException('Unable to rename the uploaded file');
168
                }
169
            } else {
170
                if (
171
                    is_uploaded_file($this->filename) === false || move_uploaded_file(
172
                        $this->filename,
173
                        $cleanTargetPath
174
                    ) === false
175
                ) {
176
                    throw new RuntimeException('Unable to move the uploaded file');
177
                }
178
            }
179
        } else {
180
            $stream = $this->getStream();
181
            if ($stream->isSeekable()) {
182
                $stream->rewind();
183
            }
184
            $dest = new Stream($cleanTargetPath);
185
            $bufferSize = 8192;
186
            while (!$stream->eof()) {
187
                if (!$dest->write($stream->read($bufferSize))) {
188
                    break;
189
                }
190
            }
191
            $stream->close();
192
        }
193
        $this->moved = true;
194
    }
195
196
    /**
197
     * {@inheritdoc}
198
     */
199
    public function getSize(): ?int
200
    {
201
        return $this->size;
202
    }
203
204
    /**
205
     * {@inheritdoc}
206
     */
207
    public function getError(): int
208
    {
209
        return $this->error;
210
    }
211
212
    /**
213
     * {@inheritdoc}
214
     */
215
    public function getClientFilename(): ?string
216
    {
217
        return $this->clientFilename;
218
    }
219
220
    /**
221
     * {@inheritdoc}
222
     */
223
    public function getClientMediaType(): ?string
224
    {
225
        return $this->clientMediaType;
226
    }
227
228
    /**
229
     * Normalize files according to standard
230
     * @param  array<string, array<string, mixed|UploadedFileInterface>>  $files
231
     * @return array<string, UploadedFileInterface|UploadedFileInterface[]>
232
     */
233
    public static function normalize(array $files): array
234
    {
235
        $normalized = [];
236
        foreach ($files as $name => $info) {
237
            if ($info instanceof UploadedFileInterface) {
238
                $normalized[$name] = $info;
239
                continue;
240
            }
241
242
            if (!isset($info['error'])) {
243
                if (is_array($info)) {
244
                    $normalized[$name] = static::normalize($info);
245
                }
246
                continue;
247
            }
248
249
            $normalized[$name] = [];
250
            if (!is_array($info['error'])) {
251
                $normalized[$name] = new static(
252
                    isset($info['tmp_name']) ? $info['tmp_name'] : '',
0 ignored issues
show
Bug introduced by
It seems like IssetNode ? $info['tmp_name'] : '' can also be of type Platine\Http\UploadedFileInterface; however, parameter $filenameOrStream of Platine\Http\UploadedFile::__construct() does only seem to accept Platine\Http\StreamInterface|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

252
                    /** @scrutinizer ignore-type */ isset($info['tmp_name']) ? $info['tmp_name'] : '',
Loading history...
253
                    !empty($info['size']) ? $info['size'] : null,
0 ignored issues
show
Bug introduced by
It seems like ! empty($info['size']) ? $info['size'] : null can also be of type Platine\Http\UploadedFileInterface; however, parameter $size of Platine\Http\UploadedFile::__construct() does only seem to accept integer|null, 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

253
                    /** @scrutinizer ignore-type */ !empty($info['size']) ? $info['size'] : null,
Loading history...
254
                    $info['error'],
0 ignored issues
show
Bug introduced by
It seems like $info['error'] can also be of type Platine\Http\UploadedFileInterface; however, parameter $error of Platine\Http\UploadedFile::__construct() does only seem to accept integer, 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

254
                    /** @scrutinizer ignore-type */ $info['error'],
Loading history...
255
                    !empty($info['name']) ? $info['name'] : null,
0 ignored issues
show
Bug introduced by
It seems like ! empty($info['name']) ? $info['name'] : null can also be of type Platine\Http\UploadedFileInterface; however, parameter $clientFilename of Platine\Http\UploadedFile::__construct() does only seem to accept null|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

255
                    /** @scrutinizer ignore-type */ !empty($info['name']) ? $info['name'] : null,
Loading history...
256
                    !empty($info['type']) ? $info['type'] : null,
0 ignored issues
show
Bug introduced by
It seems like ! empty($info['type']) ? $info['type'] : null can also be of type Platine\Http\UploadedFileInterface; however, parameter $clientMediaType of Platine\Http\UploadedFile::__construct() does only seem to accept null|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

256
                    /** @scrutinizer ignore-type */ !empty($info['type']) ? $info['type'] : null,
Loading history...
257
                );
258
            } else {
259
                $nestedInfo = [];
260
                foreach (array_keys($info['error']) as $key) {
261
                    $nestedInfo[$key]['tmp_name'] = isset($info['tmp_name'][$key]) ? $info['tmp_name'][$key] : '';
262
                    $nestedInfo[$key]['name'] = isset($info['name'][$key]) ? $info['name'][$key] : '';
263
                    $nestedInfo[$key]['size'] = isset($info['size'][$key]) ? $info['size'][$key] : null;
264
                    $nestedInfo[$key]['error'] = isset($info['error'][$key]) ? $info['error'][$key] : 0;
265
                    $nestedInfo[$key]['type'] = isset($info['type'][$key]) ? $info['type'][$key] : '';
266
267
                    $normalized[$name] = static::normalize($nestedInfo);
268
                }
269
            }
270
        }
271
272
        return $normalized;
273
    }
274
275
    /**
276
     * Filter the uploded file error
277
     * @param  int  $error
278
     * @return int
279
     */
280
    protected function filterError(int $error): int
281
    {
282
        $validErrors = [
283
            \UPLOAD_ERR_OK,
284
            \UPLOAD_ERR_INI_SIZE,
285
            \UPLOAD_ERR_FORM_SIZE,
286
            \UPLOAD_ERR_PARTIAL,
287
            \UPLOAD_ERR_NO_FILE,
288
            \UPLOAD_ERR_NO_TMP_DIR,
289
            \UPLOAD_ERR_CANT_WRITE,
290
            \UPLOAD_ERR_EXTENSION
291
        ];
292
293
        if (!in_array($error, $validErrors)) {
294
            throw new InvalidArgumentException('Upload error code must be a PHP file upload error.');
295
        }
296
297
        return $error;
298
    }
299
300
    /**
301
     * Filter the uploded file target path
302
     * @param  string    $targetPath
303
     * @return string
304
     */
305
    protected function filterTargetPath(string $targetPath): string
306
    {
307
        if ($targetPath === '') {
308
            throw new InvalidArgumentException('Target path can not be empty.');
309
        }
310
        return $targetPath;
311
    }
312
}
313