UploadedFile::getStream()   A
last analyzed

Complexity

Conditions 6
Paths 7

Size

Total Lines 23
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 15
CRAP Score 6

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 12
c 1
b 0
f 0
dl 0
loc 23
ccs 15
cts 15
cp 1
rs 9.2222
cc 6
nc 7
nop 0
crap 6
1
<?php 
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Shieldon\Psr7;
14
15
use Psr\Http\Message\StreamInterface;
16
use Psr\Http\Message\UploadedFileInterface;
17
use Shieldon\Psr7\Stream;
18
use InvalidArgumentException;
19
use RuntimeException;
20
21
use function file_exists;
22
use function file_put_contents;
23
use function is_string;
24
use function is_uploaded_file;
25
use function is_writable;
26
use function move_uploaded_file;
27
use function php_sapi_name;
28
use function rename;
29
30
use const UPLOAD_ERR_CANT_WRITE;
31
use const UPLOAD_ERR_EXTENSION;
32
use const UPLOAD_ERR_FORM_SIZE;
33
use const UPLOAD_ERR_INI_SIZE;
34
use const UPLOAD_ERR_NO_FILE;
35
use const UPLOAD_ERR_NO_TMP_DIR;
36
use const UPLOAD_ERR_OK;
37
use const UPLOAD_ERR_PARTIAL;
38
use const LOCK_EX;
39
40
/*
41
 * Describes a data stream.
42
 */
43
class UploadedFile implements UploadedFileInterface
44
{
45
    /**
46
     * The full path of the file provided by client.
47
     *
48
     * @var string|null
49
     */
50
    protected $file;
51
52
    /**
53
     * A stream representing the uploaded file.
54
     *
55
     * @var StreamInterface|null
56
     */
57
    protected $stream;
58
59
    /**
60
     * Is file copy to stream when first time calling getStream().
61
     *
62
     * @var bool
63
     */
64
    protected $isFileToStream = false;
65
66
    /**
67
     * The file size based on the "size" key in the $_FILES array.
68
     *
69
     * @var int|null
70
     */
71
    protected $size;
72
73
    /**
74
     * The filename based on the "name" key in the $_FILES array.
75
     *
76
     * @var string|null
77
     */
78
    protected $name;
79
80
    /**
81
     * The type of a file. This value is based on the "type" key in the $_FILES array.
82
     *
83
     * @var string|null
84
     */
85
    protected $type;
86
87
    /**
88
     * The error code associated with the uploaded file.
89
     *
90
     * @var int
91
     */
92
    protected $error;
93
94
    /**
95
     * Check if the uploaded file has been moved or not.
96
     *
97
     * @var bool
98
     */
99
    protected $isMoved = false;
100
101
    /**
102
     * The type of interface between web server and PHP.
103
     * This value is typically from `php_sapi_name`, might be changed ony for
104
     * unit testing purpose.
105
     *
106
     * @var string
107
     */
108
    private $sapi;
109
110
    /**
111
     * UploadedFile constructor.
112
     * 
113
     * @param string|StreamInterface $source The full path of a file or stream.
114
     * @param string|null            $name   The file name.
115
     * @param string|null            $type   The file media type.
116
     * @param int|null               $size   The file size in bytes.
117
     * @param int                    $error  The status code of the upload.
118
     * @param string|null            $sapi   Only assign for unit testing purpose.
119
     */
120 18
    public function __construct(
121
                $source       ,
122
        ?string $name   = null,
123
        ?string $type   = null,
124
        ?int    $size   = null,
125
        int     $error  = 0   ,
126
        ?string $sapi   = null
127
    ) {
128
129 18
        if (is_string($source)) {
130 15
            $this->file = $source;
131
132 4
        } elseif ($source instanceof StreamInterface) {
0 ignored issues
show
introduced by
$source is always a sub-type of Psr\Http\Message\StreamInterface.
Loading history...
133 3
            $this->stream = $source;
134
135
        } else {
136 1
            throw new InvalidArgumentException(
137 1
                'First argument accepts only a string or StreamInterface instance.'
138 1
            );
139
        }
140
141 17
        $this->name  = $name;
142 17
        $this->type  = $type;
143 17
        $this->size  = $size;
144 17
        $this->error = $error;
145 17
        $this->sapi  = php_sapi_name();
146
147 17
        if ($sapi) {
148 2
            $this->sapi = $sapi;
149
        }
150
    }
151
152
    /**
153
     * {@inheritdoc}
154
     */
155 4
    public function getStream(): StreamInterface
156
    {
157 4
        if ($this->isMoved) {
158 1
            throw new RuntimeException(
159 1
                'The stream has been moved.'
160 1
            );
161
        }
162
163 3
        if (!$this->isFileToStream && !$this->stream) {
164 2
            $resource = @fopen($this->file, 'r');
0 ignored issues
show
Bug introduced by
It seems like $this->file can also be of type null; however, parameter $filename of fopen() does only seem to accept 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

164
            $resource = @fopen(/** @scrutinizer ignore-type */ $this->file, 'r');
Loading history...
165 2
            if (is_resource($resource)) {
166 1
                $this->stream = new Stream($resource);
167
            }
168 2
            $this->isFileToStream = true;
169
        }
170
171 3
        if (!$this->stream) {
172 1
            throw new RuntimeException(
173 1
                'No stream is available or can be created.'
174 1
            );
175
        }
176
177 2
        return $this->stream;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 7
    public function moveTo($targetPath): void
184
    {
185 7
        if ($this->isMoved) {
186
            // Throw exception on the second or subsequent call to the method.
187 1
            throw new RuntimeException(
188 1
                'Uploaded file already moved'
189 1
            );
190
        }
191
192 6
        if (!is_writable(dirname($targetPath))) {
193
            // Throw exception if the $targetPath specified is invalid.
194 1
            throw new RuntimeException(
195 1
                sprintf(
196 1
                    'The target path "%s" is not writable.',
197 1
                    $targetPath
198 1
                )
199 1
            );
200
        }
201
202
        // Is a file..
203 5
        if (is_string($this->file) && ! empty($this->file)) {
204
205 3
            if ($this->sapi === 'cli') {
206
207 1
                if (!rename($this->file, $targetPath)) {
208
209
                    // @codeCoverageIgnoreStart
210
211
                    // Throw exception on any error during the move operation.
212
                    throw new RuntimeException(
213
                        sprintf(
214
                            'Could not rename the file to the target path "%s".',
215
                            $targetPath
216
                        )
217
                    );
218
219
                    // @codeCoverageIgnoreEnd
220
                }
221
            } else {
222
223
                if (
224 2
                    ! is_uploaded_file($this->file) || 
225 2
                    ! move_uploaded_file($this->file, $targetPath)
226
                ) {
227
                    // Throw exception on any error during the move operation.
228 3
                    throw new RuntimeException(
229 3
                        sprintf(
230 3
                            'Could not move the file to the target path "%s".',
231 3
                            $targetPath
232 3
                        )
233 3
                    );
234
                }
235
            }
236
237 2
        } elseif ($this->stream instanceof StreamInterface) {
238 2
            $content = $this->stream->getContents();
239
240 2
            file_put_contents($targetPath, $content, LOCK_EX);
241
242
            // @codeCoverageIgnoreStart
243
244
            if (!file_exists($targetPath)) {
245
                // Throw exception on any error during the move operation.
246
                throw new RuntimeException(
247
                    sprintf(
248
                        'Could not move the stream to the target path "%s".',
249
                        $targetPath
250
                    )
251
                );
252
            }
253
254
            // @codeCoverageIgnoreEnd
255
256 2
            unset($content, $this->stream);
257
        }
258
259 3
        $this->isMoved = true;
260
    }
261
262
    /**
263
     * {@inheritdoc}
264
     */
265 1
    public function getSize(): ?int
266
    {
267 1
        return $this->size;
268
    }
269
270
    /**
271
     * {@inheritdoc}
272
     */
273 1
    public function getError(): int
274
    {
275 1
        return $this->error;
276
    }
277
278
    /**
279
     * {@inheritdoc}
280
     */
281 2
    public function getClientFilename(): ?string
282
    {
283 2
        return $this->name;
284
    }
285
286
    /**
287
     * {@inheritdoc}
288
     */
289 1
    public function getClientMediaType(): ?string
290
    {
291 1
        return $this->type;
292
    }
293
294
    /*
295
    |--------------------------------------------------------------------------
296
    | Non-PSR-7 Methods.
297
    |--------------------------------------------------------------------------
298
    */
299
300
    /**
301
     * Get error message when uploading files.
302
     *
303
     * @return string
304
     */
305 2
    public function getErrorMessage(): string
306
    {
307 2
        $message = [
308 2
            UPLOAD_ERR_INI_SIZE   => 'The uploaded file exceeds the upload_max_filesize directive in php.ini',
309 2
            UPLOAD_ERR_FORM_SIZE  => 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form.',
310 2
            UPLOAD_ERR_PARTIAL    => 'The uploaded file was only partially uploaded.',
311 2
            UPLOAD_ERR_NO_FILE    => 'No file was uploaded.',
312 2
            UPLOAD_ERR_NO_TMP_DIR => 'Missing a temporary folder.',
313 2
            UPLOAD_ERR_CANT_WRITE => 'Failed to write file to disk.',
314 2
            UPLOAD_ERR_EXTENSION  => 'File upload stopped by extension.',
315 2
            UPLOAD_ERR_OK         => 'There is no error, the file uploaded with success.',
316 2
        ];
317
318 2
        return $message[$this->error] ?? 'Unknown upload error.';
319
    }
320
}
321