1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
declare(strict_types=1); |
4
|
|
|
|
5
|
|
|
namespace Nyholm\Psr7; |
6
|
|
|
|
7
|
|
|
use Psr\Http\Message\StreamInterface; |
8
|
|
|
use Symfony\Component\Debug\ErrorHandler as SymfonyLegacyErrorHandler; |
9
|
|
|
use Symfony\Component\ErrorHandler\ErrorHandler as SymfonyErrorHandler; |
10
|
|
|
|
11
|
|
|
/** |
12
|
|
|
* @author Michael Dowling and contributors to guzzlehttp/psr7 |
13
|
|
|
* @author Tobias Nyholm <[email protected]> |
14
|
|
|
* @author Martijn van der Ven <[email protected]> |
15
|
|
|
*/ |
16
|
|
|
final class Stream implements StreamInterface |
17
|
|
|
{ |
18
|
|
|
/** @var resource|null A resource reference */ |
19
|
|
|
private $stream; |
20
|
|
|
|
21
|
|
|
/** @var bool */ |
22
|
|
|
private $seekable; |
23
|
|
|
|
24
|
|
|
/** @var bool */ |
25
|
|
|
private $readable; |
26
|
|
|
|
27
|
|
|
/** @var bool */ |
28
|
|
|
private $writable; |
29
|
|
|
|
30
|
|
|
/** @var array|mixed|void|null */ |
31
|
|
|
private $uri; |
32
|
|
|
|
33
|
|
|
/** @var int|null */ |
34
|
|
|
private $size; |
35
|
|
|
|
36
|
|
|
/** @var array Hash of readable and writable stream types */ |
37
|
|
|
private const READ_WRITE_HASH = [ |
38
|
|
|
'read' => [ |
39
|
|
|
'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, |
40
|
|
|
'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, |
41
|
|
|
'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, |
42
|
|
|
'x+t' => true, 'c+t' => true, 'a+' => true, |
43
|
|
|
], |
44
|
|
|
'write' => [ |
45
|
|
|
'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, |
46
|
|
|
'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, |
47
|
|
|
'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, |
48
|
|
|
'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, |
49
|
|
|
], |
50
|
|
|
]; |
51
|
|
|
|
52
|
55 |
|
private function __construct() |
53
|
|
|
{ |
54
|
55 |
|
} |
55
|
|
|
|
56
|
|
|
/** |
57
|
|
|
* Creates a new PSR-7 stream. |
58
|
|
|
* |
59
|
|
|
* @param string|resource|StreamInterface $body |
60
|
|
|
* |
61
|
|
|
* @throws \InvalidArgumentException |
62
|
|
|
*/ |
63
|
55 |
|
public static function create($body = ''): StreamInterface |
64
|
|
|
{ |
65
|
55 |
|
if ($body instanceof StreamInterface) { |
66
|
|
|
return $body; |
67
|
|
|
} |
68
|
|
|
|
69
|
55 |
|
if (\is_string($body)) { |
70
|
21 |
|
$resource = \fopen('php://temp', 'rw+'); |
71
|
21 |
|
\fwrite($resource, $body); |
72
|
21 |
|
$body = $resource; |
73
|
|
|
} |
74
|
|
|
|
75
|
55 |
|
if (\is_resource($body)) { |
76
|
55 |
|
$new = new self(); |
77
|
55 |
|
$new->stream = $body; |
78
|
55 |
|
$meta = \stream_get_meta_data($new->stream); |
79
|
55 |
|
$new->seekable = $meta['seekable'] && 0 === \fseek($new->stream, 0, \SEEK_CUR); |
80
|
55 |
|
$new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]); |
81
|
55 |
|
$new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]); |
82
|
55 |
|
$new->uri = $new->getMetadata('uri'); |
83
|
|
|
|
84
|
55 |
|
return $new; |
85
|
|
|
} |
86
|
|
|
|
87
|
|
|
throw new \InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.'); |
88
|
|
|
} |
89
|
|
|
|
90
|
|
|
/** |
91
|
|
|
* Closes the stream when the destructed. |
92
|
|
|
*/ |
93
|
55 |
|
public function __destruct() |
94
|
|
|
{ |
95
|
55 |
|
$this->close(); |
96
|
55 |
|
} |
97
|
|
|
|
98
|
|
|
/** |
99
|
|
|
* @return string |
100
|
|
|
*/ |
101
|
15 |
|
public function __toString() |
102
|
|
|
{ |
103
|
|
|
try { |
104
|
15 |
|
if ($this->isSeekable()) { |
105
|
14 |
|
$this->seek(0); |
106
|
|
|
} |
107
|
|
|
|
108
|
15 |
|
return $this->getContents(); |
109
|
1 |
|
} catch (\Throwable $e) { |
110
|
1 |
|
if (\PHP_VERSION_ID >= 70400) { |
111
|
|
|
throw $e; |
112
|
|
|
} |
113
|
|
|
|
114
|
1 |
|
if (\is_array($errorHandler = \set_error_handler('var_dump'))) { |
115
|
1 |
|
$errorHandler = $errorHandler[0] ?? null; |
116
|
|
|
} |
117
|
1 |
|
\restore_error_handler(); |
118
|
|
|
|
119
|
1 |
|
if ($e instanceof \Error || $errorHandler instanceof SymfonyErrorHandler || $errorHandler instanceof SymfonyLegacyErrorHandler) { |
120
|
1 |
|
return \trigger_error((string) $e, \E_USER_ERROR); |
|
|
|
|
121
|
|
|
} |
122
|
|
|
|
123
|
1 |
|
return ''; |
124
|
|
|
} |
125
|
|
|
} |
126
|
|
|
|
127
|
55 |
|
public function close(): void |
128
|
|
|
{ |
129
|
55 |
|
if (isset($this->stream)) { |
130
|
53 |
|
if (\is_resource($this->stream)) { |
131
|
53 |
|
\fclose($this->stream); |
132
|
|
|
} |
133
|
53 |
|
$this->detach(); |
134
|
|
|
} |
135
|
55 |
|
} |
136
|
|
|
|
137
|
55 |
|
public function detach() |
138
|
|
|
{ |
139
|
55 |
|
if (!isset($this->stream)) { |
140
|
1 |
|
return null; |
141
|
|
|
} |
142
|
|
|
|
143
|
55 |
|
$result = $this->stream; |
144
|
55 |
|
unset($this->stream); |
145
|
55 |
|
$this->size = $this->uri = null; |
146
|
55 |
|
$this->readable = $this->writable = $this->seekable = false; |
147
|
|
|
|
148
|
55 |
|
return $result; |
149
|
|
|
} |
150
|
|
|
|
151
|
18 |
|
public function getSize(): ?int |
152
|
|
|
{ |
153
|
18 |
|
if (null !== $this->size) { |
154
|
2 |
|
return $this->size; |
155
|
|
|
} |
156
|
|
|
|
157
|
18 |
|
if (!isset($this->stream)) { |
158
|
2 |
|
return null; |
159
|
|
|
} |
160
|
|
|
|
161
|
|
|
// Clear the stat cache if the stream has a URI |
162
|
16 |
|
if ($this->uri) { |
163
|
16 |
|
\clearstatcache(true, $this->uri); |
164
|
|
|
} |
165
|
|
|
|
166
|
16 |
|
$stats = \fstat($this->stream); |
167
|
16 |
|
if (isset($stats['size'])) { |
168
|
16 |
|
$this->size = $stats['size']; |
169
|
|
|
|
170
|
16 |
|
return $this->size; |
171
|
|
|
} |
172
|
|
|
|
173
|
|
|
return null; |
174
|
|
|
} |
175
|
|
|
|
176
|
3 |
|
public function tell(): int |
177
|
|
|
{ |
178
|
3 |
|
if (false === $result = \ftell($this->stream)) { |
179
|
|
|
throw new \RuntimeException('Unable to determine stream position'); |
180
|
|
|
} |
181
|
|
|
|
182
|
2 |
|
return $result; |
183
|
|
|
} |
184
|
|
|
|
185
|
8 |
|
public function eof(): bool |
186
|
|
|
{ |
187
|
8 |
|
return !$this->stream || \feof($this->stream); |
188
|
|
|
} |
189
|
|
|
|
190
|
24 |
|
public function isSeekable(): bool |
191
|
|
|
{ |
192
|
24 |
|
return $this->seekable; |
193
|
|
|
} |
194
|
|
|
|
195
|
28 |
|
public function seek($offset, $whence = \SEEK_SET): void |
196
|
|
|
{ |
197
|
28 |
|
if (!$this->seekable) { |
198
|
2 |
|
throw new \RuntimeException('Stream is not seekable'); |
199
|
|
|
} |
200
|
|
|
|
201
|
26 |
|
if (-1 === \fseek($this->stream, $offset, $whence)) { |
202
|
|
|
throw new \RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . \var_export($whence, true)); |
203
|
|
|
} |
204
|
26 |
|
} |
205
|
|
|
|
206
|
8 |
|
public function rewind(): void |
207
|
|
|
{ |
208
|
8 |
|
$this->seek(0); |
209
|
7 |
|
} |
210
|
|
|
|
211
|
5 |
|
public function isWritable(): bool |
212
|
|
|
{ |
213
|
5 |
|
return $this->writable; |
214
|
|
|
} |
215
|
|
|
|
216
|
9 |
|
public function write($string): int |
217
|
|
|
{ |
218
|
9 |
|
if (!$this->writable) { |
219
|
2 |
|
throw new \RuntimeException('Cannot write to a non-writable stream'); |
220
|
|
|
} |
221
|
|
|
|
222
|
|
|
// We can't know the size after writing anything |
223
|
8 |
|
$this->size = null; |
224
|
|
|
|
225
|
8 |
|
if (false === $result = \fwrite($this->stream, $string)) { |
226
|
|
|
throw new \RuntimeException('Unable to write to stream'); |
227
|
|
|
} |
228
|
|
|
|
229
|
8 |
|
return $result; |
230
|
|
|
} |
231
|
|
|
|
232
|
5 |
|
public function isReadable(): bool |
233
|
|
|
{ |
234
|
5 |
|
return $this->readable; |
235
|
|
|
} |
236
|
|
|
|
237
|
9 |
|
public function read($length): string |
238
|
|
|
{ |
239
|
9 |
|
if (!$this->readable) { |
240
|
2 |
|
throw new \RuntimeException('Cannot read from non-readable stream'); |
241
|
|
|
} |
242
|
|
|
|
243
|
7 |
|
return \fread($this->stream, $length); |
244
|
|
|
} |
245
|
|
|
|
246
|
17 |
|
public function getContents(): string |
247
|
|
|
{ |
248
|
17 |
|
if (!isset($this->stream)) { |
249
|
1 |
|
throw new \RuntimeException('Unable to read stream contents'); |
250
|
|
|
} |
251
|
|
|
|
252
|
16 |
|
if (false === $contents = \stream_get_contents($this->stream)) { |
253
|
|
|
throw new \RuntimeException('Unable to read stream contents'); |
254
|
|
|
} |
255
|
|
|
|
256
|
16 |
|
return $contents; |
257
|
|
|
} |
258
|
|
|
|
259
|
55 |
|
public function getMetadata($key = null) |
260
|
|
|
{ |
261
|
55 |
|
if (!isset($this->stream)) { |
262
|
1 |
|
return $key ? null : []; |
263
|
|
|
} |
264
|
|
|
|
265
|
55 |
|
$meta = \stream_get_meta_data($this->stream); |
266
|
|
|
|
267
|
55 |
|
if (null === $key) { |
268
|
1 |
|
return $meta; |
269
|
|
|
} |
270
|
|
|
|
271
|
55 |
|
return $meta[$key] ?? null; |
272
|
|
|
} |
273
|
|
|
} |
274
|
|
|
|
If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.
Let’s take a look at an example:
Our function
my_function
expects aPost
object, and outputs the author of the post. The base classPost
returns a simple string and outputting a simple string will work just fine. However, the child classBlogPost
which is a sub-type ofPost
instead decided to return anobject
, and is therefore violating the SOLID principles. If aBlogPost
were passed tomy_function
, PHP would not complain, but ultimately fail when executing thestrtoupper
call in its body.