Issues (41)

src/Message/Stream.php (6 issues)

1
<?php
2
3
namespace DMT\Aura\Psr\Message;
4
5
use InvalidArgumentException;
6
use Psr\Http\Message\StreamInterface;
7
use RuntimeException;
8
9
/**
10
 * Class Stream
11
 *
12
 * @package DMT\Aura\Psr\Message
13
 */
14
class Stream implements StreamInterface
15
{
16
    /** readable mode */
17
    const MODE_READABLE = '~^(rw?|[acwx](?=b?\+))$~';
18
    /** writeable mode */
19
    const MODE_WRITEABLE = '~^([acwx]|r(?=b?[\+w]))$~';
20
21
    /** @var resource $resource */
22
    private $resource;
23
    /** @var array $metadata */
24
    private $metadata;
25
26
    /**
27
     * Stream constructor.
28
     *
29
     * @param string|resource $content
30
     */
31 93
    public function __construct($content)
32
    {
33 93
        if (!is_resource($content) && !is_string($content)) {
0 ignored issues
show
The condition is_string($content) is always true.
Loading history...
34
            throw new InvalidArgumentException('content string or resource expected');
35
        }
36
37 93
        $resource = $content;
38 93
        if (is_string($content)) {
39 66
            $resource = fopen('data://text/plain,' . $content, 'r+');
40
        }
41
42 93
        $this->resource = $resource;
0 ignored issues
show
Documentation Bug introduced by
It seems like $resource can also be of type string. However, the property $resource is declared as type resource. 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...
43 93
    }
44
45
    /**
46
     * Reads all data from the stream into a string, from the beginning to end.
47
     *
48
     * @return string
49
     */
50 4
    public function __toString()
51
    {
52
        try {
53 4
            $this->rewind();
54
55 4
            return $this->getContents();
56
        } catch (RuntimeException $exception) {
57
            return '';
58
        }
59
    }
60
61
    /**
62
     * Returns the remaining contents in a string
63
     *
64
     * @return string
65
     * @throws RuntimeException
66
     */
67 8
    public function getContents()
68
    {
69 8
        if (!is_resource($this->resource)) {
70
            throw new RuntimeException('could not read stream');
71
        }
72
73 8
        $contents = stream_get_contents($this->resource);
74 8
        if ($contents === false) {
75
            throw new RuntimeException('error whilst reading stream');
76
        }
77
78 8
        return $contents;
79
    }
80
81
    /**
82
     * Closes the stream and any underlying resources.
83
     *
84
     * @return void
85
     */
86 43
    public function close()
87
    {
88 43
        if (is_resource($this->resource)) {
89 43
            fclose($this->resource);
90
        }
91 43
    }
92
93
    /**
94
     * Separates any underlying resources from the stream.
95
     *
96
     * @return resource|null
97
     */
98 1
    public function detach()
99
    {
100 1
        if (!is_resource($this->resource)) {
101
            return null;
102
        }
103
104 1
        $handle = $this->resource;
105 1
        $this->close();
106
107 1
        return $handle;
108
    }
109
110
    /**
111
     * Get the size of the stream if known.
112
     *
113
     * @return int|null
114
     */
115 1
    public function getSize()
116
    {
117 1
        if (!is_resource($this->resource)) {
118
            return null;
119
        }
120
121 1
        return fstat($this->resource)['size'] ?? null;
122
    }
123
124
    /**
125
     * Returns the current position of the file read/write pointer
126
     *
127
     * @return int Position of the file pointer
128
     * @throws RuntimeException
129
     */
130 1
    public function tell()
131
    {
132 1
        if (!is_resource($this->resource)) {
133
            throw new RuntimeException('could not read stream');
134
        }
135
136 1
        $result = ftell($this->resource);
137 1
        if ($result === false) {
138
            throw new RuntimeException('could not determine stream position');
139
        }
140
141 1
        return $result;
142
    }
143
144
    /**
145
     * Returns true if the stream is at the end of the stream.
146
     *
147
     * @return bool
148
     */
149 5
    public function eof()
150
    {
151 5
        return is_resource($this->resource) ? feof($this->resource) : true;
152
    }
153
154
    /**
155
     * Returns whether or not the stream is seekable.
156
     *
157
     * @return bool
158
     */
159 17
    public function isSeekable()
160
    {
161 17
        return $this->getMetadata('seekable');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->getMetadata('seekable') also could return the type array which is incompatible with the documented return type boolean.
Loading history...
162
    }
163
164
    /**
165
     * Seek to a position in the stream.
166
     *
167
     * @param int $offset Stream offset
168
     * @param int $whence Specifies how the cursor position will be calculated based on the seek offset.
169
     * @throws RuntimeException on failure.
170
     */
171 15
    public function seek($offset, $whence = SEEK_SET)
172
    {
173 15
        if (!is_resource($this->resource)) {
174
            throw new RuntimeException('could not read stream');
175
        }
176
177 15
        if (!$this->isSeekable()) {
178 1
            throw new RuntimeException('stream is not seekable');
179
        }
180
181 14
        if (fseek($this->resource, $offset, (int)$whence) === -1) {
182
            throw new RuntimeException('unable to seek stream to position');
183
        }
184 14
    }
185
186
    /**
187
     * Seek to the beginning of the stream.
188
     *
189
     * @throws RuntimeException
190
     */
191 12
    public function rewind()
192
    {
193 12
        $this->seek(0);
194 11
    }
195
196
    /**
197
     * Returns whether or not the stream is writable.
198
     *
199
     * @return bool
200
     */
201 4
    public function isWritable()
202
    {
203 4
        return preg_match('~([acwx]|r(?=b?[+w]))~', $this->getMetadata('mode')) > 0;
0 ignored issues
show
It seems like $this->getMetadata('mode') can also be of type array and null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

203
        return preg_match('~([acwx]|r(?=b?[+w]))~', /** @scrutinizer ignore-type */ $this->getMetadata('mode')) > 0;
Loading history...
204
    }
205
206
    /**
207
     * Write data to the stream.
208
     *
209
     * @param string $string The string that is to be written.
210
     * @return int Returns the number of bytes written to the stream.
211
     * @throws RuntimeException on failure.
212
     */
213 2
    public function write($string)
214
    {
215 2
        if (!$this->isWritable()) {
216
            throw new RuntimeException('cannot write to stream');
217
        }
218
219 2
        $bytes = fwrite($this->resource, $string);
220 2
        if ($bytes === false) {
221
            throw new RuntimeException('error whilst writing to stream');
222
        }
223
224 2
        return $bytes;
225
    }
226
227
    /**
228
     * Returns whether or not the stream is readable.
229
     *
230
     * @return bool
231
     */
232 20
    public function isReadable()
233
    {
234 20
        return preg_match('~(rw?|[acwx](?=b?\+))~', $this->getMetadata('mode')) > 0;
0 ignored issues
show
It seems like $this->getMetadata('mode') can also be of type array and null; however, parameter $subject of preg_match() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

234
        return preg_match('~(rw?|[acwx](?=b?\+))~', /** @scrutinizer ignore-type */ $this->getMetadata('mode')) > 0;
Loading history...
235
    }
236
237
    /**
238
     * Read data from the stream.
239
     *
240
     * @param int $length
241
     * @return string
242
     * @throws RuntimeException
243
     */
244 6
    public function read($length)
245
    {
246 6
        if (!$this->isReadable()) {
247
            throw new RuntimeException('could not read from stream');
248
        }
249
250 6
        if ((int)$length < 1) {
251
            return '';
252
        }
253
254 6
        $part = fread($this->resource, (int)$length);
255 6
        if ($part === false) {
256
            throw new RuntimeException('error whilst reading the stream');
257
        }
258
259 6
        return $part;
260
    }
261
262
    /**
263
     * Get stream metadata as an associative array or retrieve a specific key.
264
     *
265
     * @param string $key Specific metadata to retrieve.
266
     * @return array|mixed|null
267
     */
268 33
    public function getMetadata($key = null)
269
    {
270 33
        if (!is_resource($this->resource)) {
271 2
            return $key === null ? [] : null;
272
        }
273
274 33
        if (empty($this->metadata)) {
275 33
            $this->metadata = stream_get_meta_data($this->resource);
276
        }
277
278 33
        if ($key === null) {
279
            return $this->metadata;
280
        }
281
282 33
        if (!is_string($key)) {
0 ignored issues
show
The condition is_string($key) is always true.
Loading history...
283
            return null;
284
        }
285
286 33
        return $this->metadata[$key] ?? null;
287
    }
288
289
    /**
290
     * Close the internal handle.
291
     */
292 43
    public function __destruct()
293
    {
294 43
        $this->close();
295 43
    }
296
}
297