Completed
Push — master ( 78d4e7...979ecb )
by smiley
02:57
created

Stream   A

Complexity

Total Complexity 40

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 0

Importance

Changes 0
Metric Value
dl 0
loc 327
rs 9.2
c 0
b 0
f 0
wmc 40
lcom 1
cbo 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 2
A __destruct() 0 3 1
A __toString() 0 16 3
A close() 0 8 2
A detach() 0 12 1
A getSize() 0 25 5
A tell() 0 9 2
A eof() 0 3 2
A isSeekable() 0 3 1
A seek() 0 10 3
A rewind() 0 3 1
A isWritable() 0 3 1
A write() 0 16 3
A isReadable() 0 3 1
A read() 0 21 5
A getContents() 0 9 2
A getMetadata() 0 13 5

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

1
<?php
2
/**
3
 * Class Stream
4
 *
5
 * @filesource   Stream.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 Exception;
16
use InvalidArgumentException;
17
use Psr\Http\Message\StreamInterface;
18
use RuntimeException;
19
20
final class Stream implements StreamInterface{
21
22
	public const MODES_READ = [
23
		'a+'  => true,
24
		'c+'  => true,
25
		'c+b' => true,
26
		'c+t' => true,
27
		'r'   => true,
28
		'r+'  => true,
29
		'rb'  => true,
30
		'rt'  => true,
31
		'r+b' => true,
32
		'r+t' => true,
33
		'w+'  => true,
34
		'w+b' => true,
35
		'w+t' => true,
36
		'x+'  => true,
37
		'x+b' => true,
38
		'x+t' => true,
39
	];
40
41
	public const MODES_WRITE = [
42
		'a'   => true,
43
		'a+'  => true,
44
		'c+'  => true,
45
		'c+b' => true,
46
		'c+t' => true,
47
		'r+'  => true,
48
		'rw'  => true,
49
		'r+b' => true,
50
		'r+t' => true,
51
		'w'   => true,
52
		'w+'  => true,
53
		'wb'  => true,
54
		'w+b' => true,
55
		'w+t' => true,
56
		'x+'  => true,
57
		'x+b' => true,
58
		'x+t' => true,
59
	];
60
61
	/**
62
	 * @var resource
63
	 */
64
	private $stream;
65
66
	/**
67
	 * @var bool
68
	 */
69
	private $seekable;
70
71
	/**
72
	 * @var bool
73
	 */
74
	private $readable;
75
76
	/**
77
	 * @var bool
78
	 */
79
	private $writable;
80
81
	/**
82
	 * @var string|null
83
	 */
84
	private $uri;
85
86
	/**
87
	 * @var int|null
88
	 */
89
	private $size;
90
91
	/**
92
	 * Stream constructor.
93
	 *
94
	 * @param resource $stream
95
	 */
96
	public function __construct($stream){
97
98
		if(!is_resource($stream)){
99
			throw new InvalidArgumentException('Stream must be a resource');
100
		}
101
102
		$this->stream = $stream;
103
104
		$meta = stream_get_meta_data($this->stream);
105
106
		$this->seekable = $meta['seekable'];
107
		$this->readable = isset($this::MODES_READ[$meta['mode']]);
108
		$this->writable = isset($this::MODES_WRITE[$meta['mode']]);
109
		$this->uri      = $meta['uri'] ?? null;
110
	}
111
112
	/**
113
	 * Closes the stream when the destructed
114
	 *
115
	 * @return void
116
	 */
117
	public function __destruct(){
118
		$this->close();
119
	}
120
121
	/**
122
	 * @inheritdoc
123
	 */
124
	public function __toString(){
125
126
		if(!is_resource($this->stream)){
127
			return '';
128
		}
129
130
		try{
131
			$this->seek(0);
132
133
			return (string)stream_get_contents($this->stream);
134
		}
135
		catch(Exception $e){
136
			return '';
137
		}
138
139
	}
140
141
	/**
142
	 * @inheritdoc
143
	 */
144
	public function close():void{
145
146
		if(is_resource($this->stream)){
147
			fclose($this->stream);
148
		}
149
150
		$this->detach();
151
	}
152
153
	/**
154
	 * @inheritdoc
155
	 */
156
	public function detach(){
157
		$oldResource = $this->stream;
158
159
		$this->stream   = null;
160
		$this->size     = null;
161
		$this->uri      = null;
162
		$this->readable = false;
163
		$this->writable = false;
164
		$this->seekable = false;
165
166
		return $oldResource;
167
	}
168
169
	/**
170
	 * @inheritdoc
171
	 */
172
	public function getSize():?int{
173
174
		if($this->size !== null){
175
			return $this->size;
176
		}
177
178
		if(!isset($this->stream)){
179
			return null;
180
		}
181
182
		// Clear the stat cache if the stream has a URI
183
		if($this->uri){
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->uri of type string|null is loosely compared to true; 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...
184
			clearstatcache(true, $this->uri);
185
		}
186
187
		$stats = fstat($this->stream);
188
189
		if(isset($stats['size'])){
190
			$this->size = $stats['size'];
191
192
			return $this->size;
193
		}
194
195
		return null;
196
	}
197
198
	/**
199
	 * @inheritdoc
200
	 */
201
	public function tell():int{
202
		$result = ftell($this->stream);
203
204
		if($result === false){
205
			throw new RuntimeException('Unable to determine stream position');
206
		}
207
208
		return $result;
209
	}
210
211
	/**
212
	 * @inheritdoc
213
	 */
214
	public function eof():bool{
215
		return !$this->stream || feof($this->stream);
216
	}
217
218
	/**
219
	 * @inheritdoc
220
	 */
221
	public function isSeekable():bool{
222
		return $this->seekable;
223
	}
224
225
	/**
226
	 * @inheritdoc
227
	 */
228
	public function seek($offset, $whence = SEEK_SET):void{
229
230
		if(!$this->seekable){
231
			throw new RuntimeException('Stream is not seekable');
232
		}
233
		elseif(fseek($this->stream, $offset, $whence) === -1){
234
			throw new RuntimeException('Unable to seek to stream position '.$offset.' with whence '.var_export($whence, true));
235
		}
236
237
	}
238
239
	/**
240
	 * @inheritdoc
241
	 */
242
	public function rewind():void{
243
		$this->seek(0);
244
	}
245
246
	/**
247
	 * @inheritdoc
248
	 */
249
	public function isWritable():bool{
250
		return $this->writable;
251
	}
252
253
	/**
254
	 * @inheritdoc
255
	 */
256
	public function write($string):int{
257
258
		if(!$this->writable){
259
			throw new RuntimeException('Cannot write to a non-writable stream');
260
		}
261
262
		// We can't know the size after writing anything
263
		$this->size = null;
264
		$result     = fwrite($this->stream, $string);
265
266
		if($result === false){
267
			throw new RuntimeException('Unable to write to stream');
268
		}
269
270
		return $result;
271
	}
272
273
	/**
274
	 * @inheritdoc
275
	 */
276
	public function isReadable():bool{
277
		return $this->readable;
278
	}
279
280
	/**
281
	 * @inheritdoc
282
	 */
283
	public function read($length):string{
284
285
		if(!$this->readable){
286
			throw new RuntimeException('Cannot read from non-readable stream');
287
		}
288
		if($length < 0){
289
			throw new RuntimeException('Length parameter cannot be negative');
290
		}
291
292
		if($length === 0){
293
			return '';
294
		}
295
296
		$string = fread($this->stream, $length);
297
298
		if($string === false){
299
			throw new RuntimeException('Unable to read from stream');
300
		}
301
302
		return $string;
303
	}
304
305
	/**
306
	 * @inheritdoc
307
	 */
308
	public function getContents():string{
309
		$contents = stream_get_contents($this->stream);
310
311
		if($contents === false){
312
			throw new RuntimeException('Unable to read stream contents');
313
		}
314
315
		return $contents;
316
	}
317
318
	/**
319
	 * Get stream metadata as an associative array or retrieve a specific key.
320
	 *
321
	 * The keys returned are identical to the keys returned from PHP's
322
	 * stream_get_meta_data() function.
323
	 *
324
	 * @link http://php.net/manual/en/function.stream-get-meta-data.php
325
	 *
326
	 * @param string $key Specific metadata to retrieve.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $key not be string|null?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

Loading history...
327
	 *
328
	 * @return array|mixed|null Returns an associative array if no key is
329
	 *     provided. Returns a specific key value if a key is provided and the
330
	 *     value is found, or null if the key is not found.
331
	 */
332
	public function getMetadata($key = null){
333
334
		if(!isset($this->stream)){
335
			return $key ? null : [];
336
		}
337
		elseif($key === null){
338
			return stream_get_meta_data($this->stream);
339
		}
340
341
		$meta = stream_get_meta_data($this->stream);
342
343
		return isset($meta[$key]) ? $meta[$key] : null;
344
	}
345
346
}
347