Completed
Push — master ( 988ea9...868c80 )
by smiley
07:26
created

UploadedFile::moveTo()   B

Complexity

Conditions 7
Paths 8

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 8
nop 1
dl 0
loc 27
rs 8.5546
c 0
b 0
f 0
1
<?php
2
/**
3
 * Class UploadedFile
4
 *
5
 * @filesource   UploadedFile.php
6
 * @created      11.08.2018
7
 * @package      chillerlan\HTTP\Psr7
8
 * @author       smiley <[email protected]>
9
 * @copyright    2018 smiley
10
 * @license      MIT
11
 */
12
13
namespace chillerlan\HTTP\Psr7;
14
15
use chillerlan\HTTP\Psr17\StreamFactory;
16
use Psr\Http\Message\{StreamInterface, UploadedFileInterface};
17
use InvalidArgumentException, RuntimeException;
18
19
use function chillerlan\HTTP\Psr17\create_stream_from_input;
20
use function in_array, is_file, is_string, is_writable, move_uploaded_file, php_sapi_name,rename;
21
22
use const UPLOAD_ERR_CANT_WRITE, UPLOAD_ERR_EXTENSION, UPLOAD_ERR_FORM_SIZE, UPLOAD_ERR_INI_SIZE,
23
	UPLOAD_ERR_NO_FILE, UPLOAD_ERR_NO_TMP_DIR, UPLOAD_ERR_OK, UPLOAD_ERR_PARTIAL;
24
25
final class UploadedFile implements UploadedFileInterface{
26
27
	/**
28
	 * @var int[]
29
	 */
30
	public const UPLOAD_ERRORS = [
31
		UPLOAD_ERR_OK,
32
		UPLOAD_ERR_INI_SIZE,
33
		UPLOAD_ERR_FORM_SIZE,
34
		UPLOAD_ERR_PARTIAL,
35
		UPLOAD_ERR_NO_FILE,
36
		UPLOAD_ERR_NO_TMP_DIR,
37
		UPLOAD_ERR_CANT_WRITE,
38
		UPLOAD_ERR_EXTENSION,
39
	];
40
41
	/**
42
	 * @var int
43
	 */
44
	private int $error;
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

Syntax error, unexpected T_STRING, expecting T_FUNCTION or T_CONST
Loading history...
45
46
	/**
47
	 * @var int
48
	 */
49
	private int $size;
50
51
	/**
52
	 * @var null|string
53
	 */
54
	private ?string $clientFilename;
55
56
	/**
57
	 * @var null|string
58
	 */
59
	private ?string $clientMediaType;
60
61
	/**
62
	 * @var null|string
63
	 */
64
	private ?string $file = null;
65
66
	/**
67
	 * @var null|\Psr\Http\Message\StreamInterface
68
	 */
69
	private ?StreamInterface $stream;
70
71
	/**
72
	 * @var bool
73
	 */
74
	private bool $moved = false;
75
76
	/**
77
	 * @var \chillerlan\HTTP\Psr17\StreamFactory
78
	 */
79
	protected StreamFactory $streamFactory;
80
81
	/**
82
	 * @param \Psr\Http\Message\StreamInterface|string|resource $file
83
	 * @param int                                               $size
84
	 * @param int                                               $error
85
	 * @param string|null                                       $filename
86
	 * @param string|null                                       $mediaType
87
	 *
88
	 * @throws \InvalidArgumentException
89
	 */
90
	public function __construct($file, int $size, int $error = UPLOAD_ERR_OK, string $filename = null, string $mediaType = null){
91
92
		if(!in_array($error, $this::UPLOAD_ERRORS, true)){
93
			throw new InvalidArgumentException('Invalid error status for UploadedFile');
94
		}
95
96
		$this->size            = (int)$size; // int type hint also accepts float...
97
		$this->error           = $error;
98
		$this->clientFilename  = $filename;
99
		$this->clientMediaType = $mediaType;
100
		$this->streamFactory   = new StreamFactory;
101
102
		if($this->error === UPLOAD_ERR_OK){
103
104
			is_string($file)
105
				? $this->file = $file
106
				: $this->stream = create_stream_from_input($file);
107
108
		}
109
110
	}
111
112
	/**
113
	 * @inheritDoc
114
	 */
115
	public function getStream():StreamInterface{
116
117
		$this->validateActive();
118
119
		if($this->stream instanceof StreamInterface){
120
			return $this->stream;
121
		}
122
123
		if(is_file($this->file)){
124
			return $this->streamFactory->createStreamFromFile($this->file, 'r+');
125
		}
126
127
		return $this->streamFactory->createStream($this->file);
128
	}
129
130
	/**
131
	 * @inheritDoc
132
	 */
133
	public function moveTo($targetPath):void{
134
135
		$this->validateActive();
136
137
		if(is_string($targetPath) && empty($targetPath)){
138
			throw new InvalidArgumentException('Invalid path provided for move operation; must be a non-empty string');
139
		}
140
141
		if(!is_writable($targetPath)){
142
			throw new RuntimeException('Directory is not writable: '.$targetPath);
143
		}
144
145
		if($this->file !== null){
146
			$this->moved = php_sapi_name() === 'cli'
147
				? rename($this->file, $targetPath)
148
				: move_uploaded_file($this->file, $targetPath);
149
		}
150
		else{
151
			$this->copyToStream($this->streamFactory->createStreamFromFile($targetPath, 'r+'));
152
			$this->moved = true;
153
		}
154
155
		if($this->moved === false){
156
			throw new RuntimeException('Uploaded file could not be moved to '.$targetPath); // @codeCoverageIgnore
157
		}
158
159
	}
160
161
	/**
162
	 * @inheritDoc
163
	 */
164
	public function getSize():?int{
165
		return $this->size;
166
	}
167
168
	/**
169
	 * @inheritDoc
170
	 */
171
	public function getError():int{
172
		return $this->error;
173
	}
174
175
	/**
176
	 * @inheritDoc
177
	 */
178
	public function getClientFilename():?string{
179
		return $this->clientFilename;
180
	}
181
182
	/**
183
	 * @inheritDoc
184
	 */
185
	public function getClientMediaType():?string{
186
		return $this->clientMediaType;
187
	}
188
189
	/**
190
	 * @return void
191
	 * @throws RuntimeException if is moved or not ok
192
	 */
193
	private function validateActive():void{
194
195
		if($this->error !== UPLOAD_ERR_OK){
196
			throw new RuntimeException('Cannot retrieve stream due to upload error');
197
		}
198
199
		if($this->moved){
200
			throw new RuntimeException('Cannot retrieve stream after it has already been moved');
201
		}
202
203
	}
204
205
	/**
206
	 * Copy the contents of a stream into another stream until the given number
207
	 * of bytes have been read.
208
	 *
209
	 * @author Michael Dowling and contributors to guzzlehttp/psr7
210
	 *
211
	 * @param StreamInterface $dest   Stream to write to
212
	 *
213
	 * @throws \RuntimeException on error
214
	 */
215
	private function copyToStream(StreamInterface $dest){
216
		$source = $this->getStream();
217
218
		if($source->isSeekable()){
219
			$source->rewind();
220
		}
221
222
		while(!$source->eof()){
223
224
			if(!$dest->write($source->read(1048576))){
225
				break; // @codeCoverageIgnore
226
			}
227
228
		}
229
230
	}
231
232
}
233