Completed
Push — master ( cfad91...f5112d )
by Filipe
02:20
created

UploadedFile   B

Complexity

Total Complexity 41

Size/Duplication

Total Lines 416
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 99.13%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 41
c 1
b 0
f 0
lcom 1
cbo 4
dl 0
loc 416
ccs 114
cts 115
cp 0.9913
rs 8.2769

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
A getStream() 0 9 2
A getSize() 0 4 1
A getError() 0 4 1
A getClientFilename() 0 4 1
A getClientMediaType() 0 4 1
A setFile() 0 13 3
A isNonSAPIEnvironment() 0 5 3
B moveTo() 0 22 4
A setError() 0 14 4
A setSize() 0 10 2
A setStream() 0 14 4
A setClientFilename() 0 11 3
A setClientMediaType() 0 11 3
A checkBeforeMove() 0 13 3
A moveUploadedFile() 0 9 2
A write() 0 16 3

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. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

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
 * This file is part of slick/http package
5
 *
6
 * For the full copyright and license information, please view the LICENSE
7
 * file that was distributed with this source code.
8
 */
9
10
namespace Slick\Http\Server;
11
12
use Psr\Http\Message\StreamInterface;
13
use Psr\Http\Message\UploadedFileInterface;
14
use Slick\Http\Exception\FileOperationException;
15
use Slick\Http\Exception\InvalidArgumentException;
16
use Slick\Http\Stream;
17
18
/**
19
 * Value object representing a file uploaded through an HTTP request.
20
 *
21
 * Instances of this interface are considered immutable; all methods that
22
 * might change state MUST be implemented such that they retain the internal
23
 * state of the current instance and return an instance that contains the
24
 * changed state.
25
 *
26
 * @package Slick\Http\Server
27
 * @author  Filipe Silva <[email protected]>
28
 */
29
class UploadedFile implements UploadedFileInterface
30
{
31
32
    /**
33
     * @var string
34
     */
35
    private $clientFilename;
36
37
    /**
38
     * @var string
39
     */
40
    private $clientMediaType;
41
42
    /**
43
     * @readwrite
44
     * @var int
45
     */
46
    private $error;
47
48
    /**
49
     * @readwrite
50
     * @var int
51
     */
52
    private $size;
53
54
    /**
55
     * @var null|StreamInterface
56
     */
57
    private $stream;
58
59
    /**
60
     * @var null|string
61
     */
62
    private $file;
63
64
    /**
65
     * @var bool Moved file flag
66
     */
67
    private $moved = false;
68
69
    /**
70
     * UploadedFile constructor.
71
     *
72
     * @param string|resource|StreamInterface $file
73
     * @param int $size
74
     * @param int $error
75
     * @param string $clientFilename
76
     * @param string $clientMediaType
77
     *
78
     * @throws InvalidArgumentException If provided file is not a
79
     *   resource, stream or valid file
80
     */
81 32
    public function __construct(
82
        $file, $size, $error, $clientFilename = null, $clientMediaType = null
83
    ) {
84 32
        $this->setError($error)
85 32
            ->setSize($size)
86 32
            ->setFile($file)
87 30
            ->setClientFilename($clientFilename)
88 30
            ->setClientMediaType($clientMediaType);
89 30
    }
90
91
    /**
92
     * Retrieve a stream representing the uploaded file.
93
     *
94
     * This method MUST return a StreamInterface instance, representing the
95
     * uploaded file. The purpose of this method is to allow utilizing native PHP
96
     * stream functionality to manipulate the file upload, such as
97
     * stream_copy_to_stream() (though the result will need to be decorated in a
98
     * native PHP stream wrapper to work with such functions).
99
     *
100
     * If the moveTo() method has been called previously, this method MUST raise
101
     * an exception.
102
     *
103
     * @return StreamInterface Stream representation of the uploaded file.
104
     * @throws \RuntimeException in cases when no stream is available or can be
105
     *     created.
106
     */
107 6
    public function getStream()
108
    {
109 6
        $this->checkBeforeMove();
110 6
        if ($this->stream instanceof StreamInterface) {
111 4
            return $this->stream;
112
        }
113 2
        $this->stream = new Stream($this->file);
114 2
        return $this->stream;
115
    }
116
117
    /**
118
     * Move the uploaded file to a new location.
119
     *
120
     * Use this method as an alternative to move_uploaded_file(). This method is
121
     * guaranteed to work in both SAPI and non-SAPI environments.
122
     * Implementations must determine which environment they are in, and use the
123
     * appropriate method (move_uploaded_file(), rename(), or a stream
124
     * operation) to perform the operation.
125
     *
126
     * $targetPath may be an absolute path, or a relative path. If it is a
127
     * relative path, resolution should be the same as used by PHP's rename()
128
     * function.
129
     *
130
     * The original file or stream MUST be removed on completion.
131
     *
132
     * If this method is called more than once, any subsequent calls MUST raise
133
     * an exception.
134
     *
135
     * When used in an SAPI environment where $_FILES is populated, when writing
136
     * files via moveTo(), is_uploaded_file() and move_uploaded_file() SHOULD be
137
     * used to ensure permissions and upload status are verified correctly.
138
     *
139
     * If you wish to move to a stream, use getStream(), as SAPI operations
140
     * cannot guarantee writing to stream destinations.
141
     *
142
     * @see http://php.net/is_uploaded_file
143
     * @see http://php.net/move_uploaded_file
144
     * @param string $targetPath Path to which to move the uploaded file.
145
     * @throws \InvalidArgumentException if the $path specified is invalid.
146
     * @throws \RuntimeException on any error during the move operation, or on
147
     *     the second or subsequent call to the method.
148
     *
149
     * @return bool
150
     */
151 12
    public function moveTo($targetPath)
152
    {
153 12
        $this->checkBeforeMove();
154
155 10
        if (! is_string($targetPath)) {
156 2
            throw new InvalidArgumentException(
157 1
                'Invalid path provided for move operation; must be a string'
158 1
            );
159
        }
160 8
        if (empty($targetPath)) {
161 2
            throw new InvalidArgumentException(
162
                'Invalid path provided for move operation; '.
163 1
                'must be a non-empty string'
164 1
            );
165
        }
166
167 6
        $this->moved = $this->isNonSAPIEnvironment()
168 5
            ? $this->write($targetPath)
169 3
            : $this->moveUploadedFile($targetPath);
170
171 2
        return $this->moved;
172
    }
173
174
    /**
175
     * Retrieve the file size.
176
     *
177
     * Implementations SHOULD return the value stored in the "size" key of
178
     * the file in the $_FILES array if available, as PHP calculates this based
179
     * on the actual size transmitted.
180
     *
181
     * @return int|null The file size in bytes or null if unknown.
182
     */
183 2
    public function getSize()
184
    {
185 2
        return $this->size;
186
    }
187
188
    /**
189
     * Retrieve the error associated with the uploaded file.
190
     *
191
     * The return value MUST be one of PHP's UPLOAD_ERR_XXX constants.
192
     *
193
     * If the file was uploaded successfully, this method MUST return
194
     * UPLOAD_ERR_OK.
195
     *
196
     * Implementations SHOULD return the value stored in the "error" key of
197
     * the file in the $_FILES array.
198
     *
199
     * @see http://php.net/manual/en/features.file-upload.errors.php
200
     * @return int One of PHP's UPLOAD_ERR_XXX constants.
201
     */
202 2
    public function getError()
203
    {
204 2
        return $this->error;
205
    }
206
207
    /**
208
     * Retrieve the filename sent by the client.
209
     *
210
     * Do not trust the value returned by this method. A client could send
211
     * a malicious filename with the intention to corrupt or hack your
212
     * application.
213
     *
214
     * Implementations SHOULD return the value stored in the "name" key of
215
     * the file in the $_FILES array.
216
     *
217
     * @return string|null The filename sent by the client or null if none
218
     *     was provided.
219
     */
220 2
    public function getClientFilename()
221
    {
222 2
        return $this->clientFilename;
223
    }
224
225
    /**
226
     * Retrieve the media type sent by the client.
227
     *
228
     * Do not trust the value returned by this method. A client could send
229
     * a malicious media type with the intention to corrupt or hack your
230
     * application.
231
     *
232
     * Implementations SHOULD return the value stored in the "type" key of
233
     * the file in the $_FILES array.
234
     *
235
     * @return string|null The media type sent by the client or null if none
236
     *     was provided.
237
     */
238 2
    public function getClientMediaType()
239
    {
240 2
        return $this->clientMediaType;
241
    }
242
243
    /**
244
     * Sets the upload status error
245
     *
246
     * @param int $error
247
     *
248
     * @return $this|self|UploadedFileInterface
249
     *
250
     * @throws InvalidArgumentException If error value is not one of
251
     *      UPLOAD_ERR_* constants
252
     */
253 32
    protected function setError($error)
254
    {
255 32
        if (! is_int($error)
256 32
            || 0 > $error
257 32
            || 8 < $error
258 16
        ) {
259 2
            throw new InvalidArgumentException(
260
                'Invalid error status for UploadedFile; must be an '.
261 1
                'UPLOAD_ERR_* constant'
262 1
            );
263
        }
264 32
        $this->error = $error;
265 32
        return $this;
266
    }
267
268
    /**
269
     * Sets the size of this  file
270
     *
271
     * @param $size
272
     *
273
     * @return self|$this|UploadedFileInterface
274
     */
275 32
    protected function setSize($size)
276
    {
277 32
        if (! is_int($size)) {
278 2
            throw new InvalidArgumentException(
279 1
                'Invalid size provided for UploadedFile; must be an int'
280 1
            );
281
        }
282 32
        $this->size = $size;
283 32
        return $this;
284
    }
285
286
    /**
287
     * Sets the source file path
288
     *
289
     * If the file is not a string, it will try to create it as a stream
290
     * using UploadedFile::setStream() method.
291
     *
292
     * @param string|resource|StreamInterface $file
293
     *
294
     * @return $this|UploadedFileInterface|UploadedFile
295
     *
296
     * @throws InvalidArgumentException If provided file is not a
297
     *   resource, stream or valid file
298
     */
299 32
    protected function setFile($file)
300
    {
301 32
        if ($this->error !== UPLOAD_ERR_OK) {
302 4
            return $this;
303
        }
304
305 28
        if (! is_string($file)) {
306 8
            return $this->setStream($file);
307
        }
308
309 20
        $this->file = $file;
310 20
        return $this;
311
    }
312
313
    /**
314
     * Creates stream if provided file is a resource our a stream interface
315
     *
316
     * @param StreamInterface|resource $file
317
     *
318
     * @return $this|self|UploadedFileInterface
319
     *
320
     * @throws InvalidArgumentException If provided file is not a
321
     *   resource, stream or valid file
322
     */
323 8
    protected function setStream($file)
324
    {
325 8
        $isStream = $file instanceof StreamInterface;
326 8
        if (! is_resource($file) && ! $isStream) {
327 2
            throw new InvalidArgumentException(
328 1
                'Invalid stream or file provided for UploadedFile'
329 1
            );
330
        }
331
332 6
        $this->stream = ($isStream)
333 6
            ? $file
334 3
            : new Stream($file);
335 6
        return $this;
336
    }
337
338
    /**
339
     * Sets client original file name
340
     *
341
     * @param string|null $clientFilename
342
     *
343
     * @return $this|self|UploadedFileInterface
344
     */
345 30
    protected function setClientFilename($clientFilename)
346
    {
347 30
        if (null !== $clientFilename && ! is_string($clientFilename)) {
348 2
            throw new InvalidArgumentException(
349
                'Invalid client filename provided for UploadedFile; '.
350 1
                'must be null or a string'
351 1
            );
352
        }
353 30
        $this->clientFilename = $clientFilename;
354 30
        return $this;
355
    }
356
357
    /**
358
     * Sets original client file media type
359
     *
360
     * @param string|null $clientMediaType
361
     *
362
     * @return $this
363
     */
364 30
    protected function setClientMediaType($clientMediaType)
365
    {
366 30
        if (null !== $clientMediaType && ! is_string($clientMediaType)) {
367 2
            throw new InvalidArgumentException(
368
                'Invalid client media type provided for UploadedFile; '.
369 1
                'must be null or a string'
370 1
            );
371
        }
372 30
        $this->clientMediaType = $clientMediaType;
373 30
        return $this;
374
    }
375
376
    /**
377
     * Check settings before upload the file.
378
     */
379 16
    protected function checkBeforeMove()
380
    {
381 16
        if ($this->error !== UPLOAD_ERR_OK) {
382 2
            throw new FileOperationException(
383 1
                'Cannot retrieve stream due to upload error'
384 1
            );
385
        }
386 14
        if ($this->moved) {
387 2
            throw new FileOperationException(
388 1
                'Cannot retrieve stream after it has already been moved'
389 1
            );
390
        }
391 14
    }
392
393
    /**
394
     * Check if running in Non-SAPI environment, or no filename present
395
     *
396
     * @return bool
397
     */
398 4
    protected function isNonSAPIEnvironment()
399
    {
400 4
        $sapi = PHP_SAPI;
401 4
        return (empty($sapi) || 0 === strpos($sapi, 'cli') || ! $this->file);
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->file of type null|string is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
402
    }
403
404
    /**
405
     * Move current uploaded file to provided target destination
406
     * @param $target
407
     *
408
     * @return bool
409
     */
410 2
    protected function moveUploadedFile($target)
411
    {
412 2
        if (false === move_uploaded_file($this->file, $target)) {
413 2
            throw new FileOperationException(
414 1
                'Error occurred while moving uploaded file'
415 1
            );
416
        }
417
        return true;
418
    }
419
420
    /**
421
     * Reads current stream and outputs it to provided path
422
     *
423
     * @param string $path
424
     * @return bool
425
     *
426
     * @throws FileOperationException If Unable to write to provided path
427
     */
428 4
    protected function write($path)
429
    {
430 4
        $handle = @fopen($path, 'wb+');
431 4
        if (false === $handle) {
432 2
            throw new FileOperationException(
433 1
                'Unable to write to designated path'
434 1
            );
435
        }
436 2
        $stream = $this->getStream();
437 2
        $stream->rewind();
438 2
        while (! $stream->eof()) {
439 2
            fwrite($handle, $stream->read(4096));
440 1
        }
441 2
        fclose($handle);
442 2
        return true;
443
    }
444
}