Passed
Push — main ( 0a94ad...30f64e )
by smiley
09:47
created

Stream::__construct()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 2
eloc 8
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 12
rs 10
1
<?php
2
/**
3
 * Class Stream
4
 *
5
 * @created      11.08.2018
6
 * @author       smiley <[email protected]>
7
 * @copyright    2018 smiley
8
 * @license      MIT
9
 */
10
11
namespace chillerlan\HTTP\Psr7;
12
13
use chillerlan\HTTP\Psr17\FactoryHelpers;
14
use InvalidArgumentException, RuntimeException;
15
use Psr\Http\Message\StreamInterface;
16
use function clearstatcache, fclose, feof, fread, fstat, ftell, fwrite, in_array,
17
	is_resource, stream_get_contents, stream_get_meta_data;
18
19
use const SEEK_SET;
20
21
/**
22
 * @property resource|null $stream
23
 */
24
class Stream implements StreamInterface{
25
26
	/** @var resource|null */
27
	protected $stream = null;
28
29
	protected bool $seekable;
30
31
	protected bool $readable;
32
33
	protected bool $writable;
34
35
	protected ?string $uri = null;
36
37
	protected ?int $size = null;
38
39
	/**
40
	 * Stream constructor.
41
	 *
42
	 * @param resource $stream
43
	 */
44
	public function __construct($stream){
45
46
		if(!is_resource($stream)){
47
			throw new InvalidArgumentException('Stream must be a resource');
48
		}
49
50
		$this->stream   = $stream;
51
		$meta           = $this->getMetadata();
52
		$this->seekable = $meta['seekable'] ?? false;
53
		$this->readable = in_array($meta['mode'], FactoryHelpers::STREAM_MODES_READ);
54
		$this->writable = in_array($meta['mode'], FactoryHelpers::STREAM_MODES_WRITE);
55
		$this->uri      = $meta['uri'] ?? null;
56
	}
57
58
	/**
59
	 * Closes the stream when the destructed
60
	 *
61
	 * @return void
62
	 */
63
	public function __destruct(){
64
		$this->close();
65
	}
66
67
	/**
68
	 * @inheritDoc
69
	 */
70
	public function __toString(){
71
72
		if(!is_resource($this->stream)){
73
			return '';
74
		}
75
76
		if($this->isSeekable()){
77
			$this->seek(0);
78
		}
79
80
		// this would be nice but some iplementations don't like nice things :(
81
#		$wrapper_type = $this->getMetadata('wrapper_type');
82
#		if($wrapper_type === 'plainfile'){
83
#			return $this->getMetadata('uri');
84
#		}
85
86
		return $this->getContents();
87
	}
88
89
	/**
90
	 * @inheritDoc
91
	 */
92
	public function close():void{
93
94
		if(is_resource($this->stream)){
95
			fclose($this->stream);
96
		}
97
98
		$this->detach();
99
	}
100
101
	/**
102
	 * @inheritDoc
103
	 */
104
	public function detach(){
105
		$oldResource = $this->stream;
106
107
		$this->stream   = null;
108
		$this->size     = null;
109
		$this->uri      = null;
110
		$this->readable = false;
111
		$this->writable = false;
112
		$this->seekable = false;
113
114
		return $oldResource;
115
	}
116
117
	/**
118
	 * @inheritDoc
119
	 */
120
	public function getSize():?int{
121
122
		if(!is_resource($this->stream)){
123
			return null;
124
		}
125
126
		// Clear the stat cache if the stream has a URI
127
		if($this->uri){
128
			clearstatcache(true, $this->uri);
129
		}
130
131
		$stats = fstat($this->stream);
132
133
		if(isset($stats['size'])){
134
			$this->size = $stats['size'];
135
136
			return $this->size;
137
		}
138
139
		if($this->size !== null){
140
			return $this->size;
141
		}
142
143
		return null; // @codeCoverageIgnore
144
	}
145
146
	/**
147
	 * @inheritDoc
148
	 */
149
	public function tell():int{
150
151
		if(!is_resource($this->stream)){
152
			throw new RuntimeException('Invalid stream'); // @codeCoverageIgnore
153
		}
154
155
		$result = ftell($this->stream);
156
157
		if($result === false){
158
			throw new RuntimeException('Unable to determine stream position'); // @codeCoverageIgnore
159
		}
160
161
		return $result;
162
	}
163
164
	/**
165
	 * @inheritDoc
166
	 */
167
	public function eof():bool{
168
		return !$this->stream || feof($this->stream);
169
	}
170
171
	/**
172
	 * @inheritDoc
173
	 */
174
	public function isSeekable():bool{
175
		return $this->seekable;
176
	}
177
178
	/**
179
	 * @inheritDoc
180
	 */
181
	public function seek($offset, $whence = SEEK_SET):void{
182
183
		if(!is_resource($this->stream)){
184
			throw new RuntimeException('Invalid stream'); // @codeCoverageIgnore
185
		}
186
187
		if(!$this->seekable){
188
			throw new RuntimeException('Stream is not seekable');
189
		}
190
		elseif(fseek($this->stream, $offset, $whence) === -1){
191
			throw new RuntimeException('Unable to seek to stream position '.$offset.' with whence '.$whence);
192
		}
193
194
	}
195
196
	/**
197
	 * @inheritDoc
198
	 */
199
	public function rewind():void{
200
		$this->seek(0);
201
	}
202
203
	/**
204
	 * @inheritDoc
205
	 */
206
	public function isWritable():bool{
207
		return $this->writable;
208
	}
209
210
	/**
211
	 * @inheritDoc
212
	 */
213
	public function write($string):int{
214
215
		if(!is_resource($this->stream)){
216
			throw new RuntimeException('Invalid stream'); // @codeCoverageIgnore
217
		}
218
219
		if(!$this->writable){
220
			throw new RuntimeException('Cannot write to a non-writable stream');
221
		}
222
223
		// We can't know the size after writing anything
224
		$this->size = null;
225
		$result     = fwrite($this->stream, $string);
226
227
		if($result === false){
228
			throw new RuntimeException('Unable to write to stream'); // @codeCoverageIgnore
229
		}
230
231
		return $result;
232
	}
233
234
	/**
235
	 * @inheritDoc
236
	 */
237
	public function isReadable():bool{
238
		return $this->readable;
239
	}
240
241
	/**
242
	 * @inheritDoc
243
	 */
244
	public function read($length):string{
245
246
		if(!is_resource($this->stream)){
247
			throw new RuntimeException('Invalid stream'); // @codeCoverageIgnore
248
		}
249
250
		if(!$this->readable){
251
			throw new RuntimeException('Cannot read from non-readable stream');
252
		}
253
254
		if($length < 0){
255
			throw new RuntimeException('Length parameter cannot be negative');
256
		}
257
258
		if($length === 0){
259
			return '';
260
		}
261
262
		$string = fread($this->stream, $length);
263
264
		if($string === false){
265
			throw new RuntimeException('Unable to read from stream'); // @codeCoverageIgnore
266
		}
267
268
		return $string;
269
	}
270
271
	/**
272
	 * @inheritDoc
273
	 */
274
	public function getContents():string{
275
276
		if(!is_resource($this->stream)){
277
			throw new RuntimeException('Invalid stream'); // @codeCoverageIgnore
278
		}
279
280
		$contents = stream_get_contents($this->stream);
281
282
		if($contents === false){
283
			throw new RuntimeException('Unable to read stream contents'); // @codeCoverageIgnore
284
		}
285
286
		return $contents;
287
	}
288
289
	/**
290
	 * @inheritDoc
291
	 */
292
	public function getMetadata($key = null){
293
294
		if(!is_resource($this->stream)){
295
			return $key ? null : [];
296
		}
297
		elseif($key === null){
298
			return stream_get_meta_data($this->stream);
299
		}
300
301
		$meta = stream_get_meta_data($this->stream);
302
303
		return $meta[$key] ?? null;
304
	}
305
306
}
307