Stream   B
last analyzed

Complexity

Total Complexity 45

Size/Duplication

Total Lines 281
Duplicated Lines 0 %

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 95
dl 0
loc 281
rs 8.8
c 8
b 0
f 0
wmc 45

17 Methods

Rating   Name   Duplication   Size   Complexity  
A getMetadata() 0 12 4
A close() 0 7 2
A __toString() 0 17 3
A __destruct() 0 2 1
A seek() 0 11 4
A read() 0 25 6
A detach() 0 11 1
A isReadable() 0 2 1
A eof() 0 2 2
A getSize() 0 24 5
A isWritable() 0 2 1
A __construct() 0 13 2
A isSeekable() 0 2 1
A write() 0 19 4
A tell() 0 13 3
A rewind() 0 2 1
A getContents() 0 17 4

How to fix   Complexity   

Complex Class

Complex classes like Stream 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 Stream, and based on these observations, apply Extract Interface, too.

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