Issues (29)

src/Http/Stream.php (13 issues)

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
Documentation Bug introduced by
It seems like $stream of type resource is incompatible with the declared type Psr\Http\Message\StreamInterface of property $stream.

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..

Loading history...
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
Documentation Bug introduced by
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 $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

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;
}
Loading history...
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 ignore-type  annotation

118
            return (string) stream_get_contents(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
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 ignore-type  annotation

133
        $contents = stream_get_contents(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
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
The condition is_resource($this->stream) is always false.
Loading history...
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
Bug Best Practice introduced by
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.
    }
}
Loading history...
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 ignore-type  annotation

190
        $stats = fstat(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
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 ignore-type  annotation

232
        return feof(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
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 ignore-type  annotation

244
        $result = ftell(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
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 ignore-type  annotation

272
        if (fseek(/** @scrutinizer ignore-type */ $this->stream, $offset, $whence) === -1) {
Loading history...
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 ignore-type  annotation

297
        $string = fread(/** @scrutinizer ignore-type */ $this->stream, $length);
Loading history...
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 ignore-type  annotation

319
        $result = fwrite(/** @scrutinizer ignore-type */ $this->stream, $string);
Loading history...
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 ignore-type  annotation

336
            return $this->customMetadata + stream_get_meta_data(/** @scrutinizer ignore-type */ $this->stream);
Loading history...
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