1 | <?php declare(strict_types=1); |
||||
2 | |||||
3 | namespace One\Http; |
||||
4 | |||||
5 | class Stream implements \Psr\Http\Message\StreamInterface |
||||
6 | { |
||||
7 | /** |
||||
8 | * Stream |
||||
9 | * @var \Psr\Http\Message\StreamInterface |
||||
10 | */ |
||||
11 | private $stream; |
||||
12 | |||||
13 | /** |
||||
14 | * Size |
||||
15 | * @var int |
||||
16 | */ |
||||
17 | private $size; |
||||
18 | |||||
19 | /** |
||||
20 | * Seekable |
||||
21 | * @var bool |
||||
22 | */ |
||||
23 | private $seekable; |
||||
24 | |||||
25 | /** |
||||
26 | * Readable |
||||
27 | * @var bool |
||||
28 | */ |
||||
29 | private $readable; |
||||
30 | |||||
31 | /** |
||||
32 | * Writeable |
||||
33 | * @var bool |
||||
34 | */ |
||||
35 | private $writable; |
||||
36 | |||||
37 | /** |
||||
38 | * uri |
||||
39 | * @var \One\Uri |
||||
40 | */ |
||||
41 | private $uri; |
||||
42 | |||||
43 | /** |
||||
44 | * Custom meta data |
||||
45 | * @var mixed |
||||
46 | */ |
||||
47 | private $customMetadata; |
||||
48 | |||||
49 | /** |
||||
50 | * array Hash of readable and writable stream types |
||||
51 | * @var array<string[]> |
||||
52 | */ |
||||
53 | private static $readWriteHash = [ |
||||
54 | 'read' => [ |
||||
55 | 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true, |
||||
56 | 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true, |
||||
57 | 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true, |
||||
58 | 'x+t' => true, 'c+t' => true, 'a+' => true, |
||||
59 | ], |
||||
60 | 'write' => [ |
||||
61 | 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true, |
||||
62 | 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, |
||||
63 | 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true, |
||||
64 | 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true, |
||||
65 | ], |
||||
66 | ]; |
||||
67 | |||||
68 | /** |
||||
69 | * This constructor accepts an associative array of options. |
||||
70 | * |
||||
71 | * - size: (int) If a read stream would otherwise have an indeterminate |
||||
72 | * size, but the size is known due to foreknowledge, then you can |
||||
73 | * provide that size, in bytes. |
||||
74 | * - metadata: (array) Any additional metadata to return when the metadata |
||||
75 | * of the stream is accessed. |
||||
76 | * |
||||
77 | * @param bool|resource $stream Stream resource to wrap. |
||||
78 | * @param array $options Associative array of options. |
||||
79 | * |
||||
80 | * @throws \InvalidArgumentException if the stream is not a stream resource |
||||
81 | */ |
||||
82 | public function __construct($stream, $options = []) |
||||
83 | { |
||||
84 | if (! is_resource($stream)) { |
||||
85 | throw new \InvalidArgumentException('Stream must be a resource'); |
||||
86 | } |
||||
87 | |||||
88 | if (isset($options['size'])) { |
||||
89 | $this->size = $options['size']; |
||||
90 | } |
||||
91 | |||||
92 | $this->customMetadata = $options['metadata'] |
||||
93 | ?? []; |
||||
94 | |||||
95 | $this->stream = $stream; |
||||
0 ignored issues
–
show
|
|||||
96 | $meta = stream_get_meta_data($this->stream); |
||||
97 | $this->seekable = $meta['seekable']; |
||||
98 | $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]); |
||||
99 | $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]); |
||||
100 | $this->uri = $this->getMetadata('uri'); |
||||
0 ignored issues
–
show
It seems like
$this->getMetadata('uri') can also be of type array . However, the property $uri is declared as type One\Uri . Maybe add an additional type check?
Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly. For example, imagine you have a variable Either this assignment is in error or a type check should be added for that assignment. class Id
{
public $id;
public function __construct($id)
{
$this->id = $id;
}
}
class Account
{
/** @var Id $id */
public $id;
}
$account_id = false;
if (starsAreRight()) {
$account_id = new Id(42);
}
$account = new Account();
if ($account instanceof Id)
{
$account->id = $account_id;
}
![]() |
|||||
101 | } |
||||
102 | |||||
103 | /** |
||||
104 | * Closes the stream when the destructed |
||||
105 | */ |
||||
106 | public function __destruct() |
||||
107 | { |
||||
108 | $this->close(); |
||||
109 | } |
||||
110 | |||||
111 | /** |
||||
112 | * @inheritDoc |
||||
113 | */ |
||||
114 | public function __toString() |
||||
115 | { |
||||
116 | try { |
||||
117 | $this->seek(0); |
||||
118 | return (string) stream_get_contents($this->stream); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of stream_get_contents() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
119 | } catch (\Throwable $e) { |
||||
120 | return ''; |
||||
121 | } |
||||
122 | } |
||||
123 | |||||
124 | /** |
||||
125 | * @inheritDoc |
||||
126 | */ |
||||
127 | public function getContents() |
||||
128 | { |
||||
129 | if (! isset($this->stream)) { |
||||
130 | throw new \RuntimeException('Stream is detached'); |
||||
131 | } |
||||
132 | |||||
133 | $contents = stream_get_contents($this->stream); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of stream_get_contents() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
134 | |||||
135 | if ($contents === false) { |
||||
136 | throw new \RuntimeException('Unable to read stream contents'); |
||||
137 | } |
||||
138 | |||||
139 | return $contents; |
||||
140 | } |
||||
141 | |||||
142 | /** |
||||
143 | * @inheritDoc |
||||
144 | */ |
||||
145 | public function close(): void |
||||
146 | { |
||||
147 | if (isset($this->stream)) { |
||||
148 | if (is_resource($this->stream)) { |
||||
0 ignored issues
–
show
|
|||||
149 | fclose($this->stream); |
||||
150 | } |
||||
151 | $this->detach(); |
||||
152 | } |
||||
153 | } |
||||
154 | |||||
155 | /** |
||||
156 | * @inheritDoc |
||||
157 | */ |
||||
158 | public function detach() |
||||
159 | { |
||||
160 | if (! isset($this->stream)) { |
||||
161 | return null; |
||||
162 | } |
||||
163 | |||||
164 | $result = $this->stream; |
||||
165 | unset($this->stream); |
||||
166 | $this->size = $this->uri = null; |
||||
167 | $this->readable = $this->writable = $this->seekable = false; |
||||
168 | |||||
169 | return $result; |
||||
0 ignored issues
–
show
The expression
return $result returns the type Psr\Http\Message\StreamInterface which is incompatible with the return type mandated by Psr\Http\Message\StreamInterface::detach() of null|resource .
In the issue above, the returned value is violating the contract defined by the mentioned interface. Let's take a look at an example: interface HasName {
/** @return string */
public function getName();
}
class Name {
public $name;
}
class User implements HasName {
/** @return string|Name */
public function getName() {
return new Name('foo'); // This is a violation of the ``HasName`` interface
// which only allows a string value to be returned.
}
}
![]() |
|||||
170 | } |
||||
171 | |||||
172 | /** |
||||
173 | * @inheritDoc |
||||
174 | */ |
||||
175 | public function getSize() |
||||
176 | { |
||||
177 | if ($this->size !== null) { |
||||
178 | return $this->size; |
||||
179 | } |
||||
180 | |||||
181 | if (! isset($this->stream)) { |
||||
182 | return null; |
||||
183 | } |
||||
184 | |||||
185 | // Clear the stat cache if the stream has a URI |
||||
186 | if ($this->uri) { |
||||
187 | clearstatcache(true, $this->uri); |
||||
188 | } |
||||
189 | |||||
190 | $stats = fstat($this->stream); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of fstat() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
191 | if (isset($stats['size'])) { |
||||
192 | $this->size = $stats['size']; |
||||
193 | return $this->size; |
||||
194 | } |
||||
195 | |||||
196 | return null; |
||||
197 | } |
||||
198 | |||||
199 | /** |
||||
200 | * @inheritDoc |
||||
201 | */ |
||||
202 | public function isReadable() |
||||
203 | { |
||||
204 | return $this->readable; |
||||
205 | } |
||||
206 | |||||
207 | /** |
||||
208 | * @inheritDoc |
||||
209 | */ |
||||
210 | public function isWritable() |
||||
211 | { |
||||
212 | return $this->writable; |
||||
213 | } |
||||
214 | |||||
215 | /** |
||||
216 | * @inheritDoc |
||||
217 | */ |
||||
218 | public function isSeekable() |
||||
219 | { |
||||
220 | return $this->seekable; |
||||
221 | } |
||||
222 | |||||
223 | /** |
||||
224 | * @inheritDoc |
||||
225 | */ |
||||
226 | public function eof() |
||||
227 | { |
||||
228 | if (! isset($this->stream)) { |
||||
229 | throw new \RuntimeException('Stream is detached'); |
||||
230 | } |
||||
231 | |||||
232 | return feof($this->stream); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of feof() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
233 | } |
||||
234 | |||||
235 | /** |
||||
236 | * @inheritDoc |
||||
237 | */ |
||||
238 | public function tell() |
||||
239 | { |
||||
240 | if (! isset($this->stream)) { |
||||
241 | throw new \RuntimeException('Stream is detached'); |
||||
242 | } |
||||
243 | |||||
244 | $result = ftell($this->stream); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of ftell() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
245 | |||||
246 | if ($result === false) { |
||||
247 | throw new \RuntimeException('Unable to determine stream position'); |
||||
248 | } |
||||
249 | |||||
250 | return $result; |
||||
251 | } |
||||
252 | |||||
253 | /** |
||||
254 | * @inheritDoc |
||||
255 | */ |
||||
256 | public function rewind(): void |
||||
257 | { |
||||
258 | $this->seek(0); |
||||
259 | } |
||||
260 | |||||
261 | /** |
||||
262 | * @inheritDoc |
||||
263 | */ |
||||
264 | public function seek($offset, $whence = SEEK_SET): void |
||||
265 | { |
||||
266 | if (! isset($this->stream)) { |
||||
267 | throw new \RuntimeException('Stream is detached'); |
||||
268 | } |
||||
269 | if (! $this->seekable) { |
||||
270 | throw new \RuntimeException('Stream is not seekable'); |
||||
271 | } |
||||
272 | if (fseek($this->stream, $offset, $whence) === -1) { |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of fseek() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
273 | throw new \RuntimeException('Unable to seek to stream position ' |
||||
274 | . $offset . ' with whence ' . var_export($whence, true)); |
||||
275 | } |
||||
276 | } |
||||
277 | |||||
278 | /** |
||||
279 | * @inheritDoc |
||||
280 | */ |
||||
281 | public function read($length) |
||||
282 | { |
||||
283 | if (! isset($this->stream)) { |
||||
284 | throw new \RuntimeException('Stream is detached'); |
||||
285 | } |
||||
286 | if (! $this->readable) { |
||||
287 | throw new \RuntimeException('Cannot read from non-readable stream'); |
||||
288 | } |
||||
289 | if ($length < 0) { |
||||
290 | throw new \RuntimeException('Length parameter cannot be negative'); |
||||
291 | } |
||||
292 | |||||
293 | if ($length === 0) { |
||||
294 | return ''; |
||||
295 | } |
||||
296 | |||||
297 | $string = fread($this->stream, $length); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of fread() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
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 write($string) |
||||
309 | { |
||||
310 | if (! isset($this->stream)) { |
||||
311 | throw new \RuntimeException('Stream is detached'); |
||||
312 | } |
||||
313 | if (! $this->writable) { |
||||
314 | throw new \RuntimeException('Cannot write to a non-writable stream'); |
||||
315 | } |
||||
316 | |||||
317 | // We can't know the size after writing anything |
||||
318 | $this->size = null; |
||||
319 | $result = fwrite($this->stream, $string); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of fwrite() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
320 | |||||
321 | if ($result === false) { |
||||
322 | throw new \RuntimeException('Unable to write to stream'); |
||||
323 | } |
||||
324 | |||||
325 | return $result; |
||||
326 | } |
||||
327 | |||||
328 | /** |
||||
329 | * @inheritDoc |
||||
330 | */ |
||||
331 | public function getMetadata($key = null) |
||||
332 | { |
||||
333 | if (! isset($this->stream)) { |
||||
334 | return $key ? null : []; |
||||
335 | } elseif (! $key) { |
||||
336 | return $this->customMetadata + stream_get_meta_data($this->stream); |
||||
0 ignored issues
–
show
$this->stream of type Psr\Http\Message\StreamInterface is incompatible with the type resource expected by parameter $stream of stream_get_meta_data() .
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
337 | } elseif (isset($this->customMetadata[$key])) { |
||||
338 | return $this->customMetadata[$key]; |
||||
339 | } |
||||
340 | |||||
341 | $meta = stream_get_meta_data($this->stream); |
||||
342 | |||||
343 | return $meta[$key] ?? null; |
||||
344 | } |
||||
345 | } |
||||
346 |
Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.
Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..