Passed
Push — master ( c30d3b...df82e6 )
by Evgeniy
01:37
created

StreamTrait::read()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 15
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.25

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 15
ccs 6
cts 8
cp 0.75
crap 4.25
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
namespace HttpSoft\Message;
6
7
use InvalidArgumentException;
8
use RuntimeException;
9
10
use function array_key_exists;
11
use function fclose;
12
use function feof;
13
use function fopen;
14
use function fread;
15
use function fseek;
16
use function fstat;
17
use function ftell;
18
use function fwrite;
19
use function get_resource_type;
20
use function is_int;
21
use function is_resource;
22
use function is_string;
23
use function stream_get_contents;
24
use function stream_get_meta_data;
25
use function strpos;
26
27
use const SEEK_SET;
28
29
/**
30
 * Trait implementing the methods defined in `Psr\Http\Message\StreamInterface`.
31
 *
32
 * @see https://github.com/php-fig/http-message/tree/master/src/StreamInterface.php
33
 */
34
trait StreamTrait
35
{
36
    /**
37
     * @var resource|null
38
     */
39
    private $resource;
40
41
    /**
42
     * Closes the stream and any underlying resources when the instance is destructed.
43
     */
44 51
    public function __destruct()
45
    {
46 51
        $this->close();
47 51
    }
48
49
    /**
50
     * Reads all data from the stream into a string, from the beginning to end.
51
     *
52
     * This method MUST attempt to seek to the beginning of the stream before
53
     * reading data and read the stream until the end is reached.
54
     *
55
     * Warning: This could attempt to load a large amount of data into memory.
56
57
     * @return string
58
     * @throws RuntimeException
59
     */
60 7
    public function __toString(): string
61
    {
62 7
        if ($this->isSeekable()) {
63 6
            $this->rewind();
64
        }
65
66 7
        return $this->getContents();
67
    }
68
69
    /**
70
     * Closes the stream and any underlying resources.
71
     *
72
     * @return void
73
     * @psalm-suppress PossiblyNullArgument
74
     */
75 57
    public function close(): void
76
    {
77 57
        if ($this->resource) {
78 55
            $resource = $this->detach();
79 55
            fclose($resource);
80
        }
81 57
    }
82
83
    /**
84
     * Separates any underlying resources from the stream.
85
     *
86
     * After the stream has been detached, the stream is in an unusable state.
87
     *
88
     * @return resource|null Underlying PHP stream, if any
89
     */
90 57
    public function detach()
91
    {
92 57
        $resource = $this->resource;
93 57
        $this->resource = null;
94 57
        return $resource;
95
    }
96
97
    /**
98
     * Get the size of the stream if known.
99
     *
100
     * @return int|null Returns the size in bytes if known, or null if unknown.
101
     */
102 14
    public function getSize(): ?int
103
    {
104 14
        if ($this->resource === null) {
105 2
            return null;
106
        }
107
108 12
        $stats = fstat($this->resource);
109 12
        return isset($stats['size']) ? (int) $stats['size'] : null;
110
    }
111
112
    /**
113
     * Returns the current position of the file read/write pointer
114
     *
115
     * @return int Position of the file pointer
116
     * @throws RuntimeException on error.
117
     */
118 3
    public function tell(): int
119
    {
120 3
        if (!$this->resource) {
121 1
            throw new RuntimeException('No resource available. Cannot tell position');
122
        }
123
124 2
        if (!is_int($result = ftell($this->resource))) {
0 ignored issues
show
introduced by
The condition is_int($result = ftell($this->resource)) is always true.
Loading history...
125
            throw new RuntimeException('Error occurred during tell operation');
126
        }
127
128 2
        return $result;
129
    }
130
131
    /**
132
     * Returns true if the stream is at the end of the stream.
133
     *
134
     * @return bool
135
     */
136 9
    public function eof(): bool
137
    {
138 9
        return (!$this->resource || feof($this->resource));
139
    }
140
141
    /**
142
     * Returns whether or not the stream is seekable.
143
     *
144
     * @return bool
145
     */
146 29
    public function isSeekable(): bool
147
    {
148 29
        return ($this->resource && $this->getMetadata('seekable'));
149
    }
150
151
    /**
152
     * Seek to a position in the stream.
153
     *
154
     * @link http://www.php.net/manual/en/function.fseek.php
155
     * @param int $offset Stream offset
156
     * @param int $whence Specifies how the cursor position will be calculated
157
     *     based on the seek offset. Valid values are identical to the built-in
158
     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
159
     *     offset bytes SEEK_CUR: Set position to current location plus offset
160
     *     SEEK_END: Set position to end-of-stream plus offset.
161
     * @throws RuntimeException on failure.
162
     */
163 24
    public function seek($offset, $whence = SEEK_SET): void
164
    {
165 24
        if (!$this->resource) {
166 1
            throw new RuntimeException('No resource available. Cannot seek position.');
167
        }
168
169 23
        if (!$this->isSeekable()) {
170 1
            throw new RuntimeException('Stream is not seekable.');
171
        }
172
173 22
        if (fseek($this->resource, $offset, $whence) !== 0) {
174
            throw new RuntimeException('Error seeking within stream.');
175
        }
176 22
    }
177
178
    /**
179
     * Seek to the beginning of the stream.
180
     *
181
     * If the stream is not seekable, this method will raise an exception;
182
     * otherwise, it will perform a seek(0).
183
     *
184
     * @throws RuntimeException on failure.
185
     * @link http://www.php.net/manual/en/function.fseek.php
186
     * @see seek()
187
     */
188 20
    public function rewind(): void
189
    {
190 20
        $this->seek(0);
191 19
    }
192
193
    /**
194
     * Returns whether or not the stream is writable.
195
     *
196
     * @return bool
197
     * @psalm-suppress MixedAssignment
198
     */
199 22
    public function isWritable(): bool
200
    {
201 22
        if (!is_string($mode = $this->getMetadata('mode'))) {
202
            return false;
203
        }
204
205
        return (
206 22
            strpos($mode, 'w') !== false
207 22
            || strpos($mode, '+') !== false
208 22
            || strpos($mode, 'x') !== false
209 22
            || strpos($mode, 'c') !== false
210 22
            || strpos($mode, 'a') !== false
211
        );
212
    }
213
214
    /**
215
     * Write data to the stream.
216
     *
217
     * @param string $string The string that is to be written.
218
     * @return int Returns the number of bytes written to the stream.
219
     * @throws RuntimeException on failure.
220
     */
221 18
    public function write($string): int
222
    {
223 18
        if (!$this->resource) {
224 1
            throw new RuntimeException('No resource available. Cannot write.');
225
        }
226
227 17
        if (!$this->isWritable()) {
228
            throw new RuntimeException('Stream is not writable.');
229
        }
230
231 17
        if (!is_int($result = fwrite($this->resource, $string))) {
0 ignored issues
show
introduced by
The condition is_int($result = fwrite(...is->resource, $string)) is always true.
Loading history...
232
            throw new RuntimeException('Error writing to stream.');
233
        }
234
235 17
        return $result;
236
    }
237
238
    /**
239
     * Returns whether or not the stream is readable.
240
     *
241
     * @return bool
242
     * @psalm-suppress MixedAssignment
243
     */
244 25
    public function isReadable(): bool
245
    {
246 25
        if (!is_string($mode = $this->getMetadata('mode'))) {
247 1
            return false;
248
        }
249
250 24
        return (strpos($mode, 'r') !== false || strpos($mode, '+') !== false);
251
    }
252
253
    /**
254
     * Read data from the stream.
255
     *
256
     * @param int $length Read up to $length bytes from the object and return
257
     *     them. Fewer than $length bytes may be returned if underlying stream
258
     *     call returns fewer bytes.
259
     * @return string Returns the data read from the stream, or an empty string
260
     *     if no bytes are available.
261
     * @throws RuntimeException if an error occurs.
262
     */
263 10
    public function read($length): string
264
    {
265 10
        if (!$this->resource) {
266 1
            throw new RuntimeException('No resource available. Cannot read.');
267
        }
268
269 9
        if (!$this->isReadable()) {
270
            throw new RuntimeException('Stream is not readable.');
271
        }
272
273 9
        if (!is_string($result = fread($this->resource, $length))) {
0 ignored issues
show
introduced by
The condition is_string($result = frea...is->resource, $length)) is always true.
Loading history...
274
            throw new RuntimeException('Error reading stream.');
275
        }
276
277 9
        return $result;
278
    }
279
280
    /**
281
     * Returns the remaining contents in a string
282
     *
283
     * @return string
284
     * @throws RuntimeException if unable to read or an error occurs while reading.
285
     * @psalm-suppress PossiblyNullArgument
286
     */
287 13
    public function getContents(): string
288
    {
289 13
        if (!$this->isReadable()) {
290 2
            throw new RuntimeException('Stream is not readable.');
291
        }
292
293 11
        if (!is_string($result = stream_get_contents($this->resource))) {
0 ignored issues
show
introduced by
The condition is_string($result = stre...tents($this->resource)) is always true.
Loading history...
294
            throw new RuntimeException('Error reading stream.');
295
        }
296
297 11
        return $result;
298
    }
299
300
    /**
301
     * Get stream metadata as an associative array or retrieve a specific key.
302
     *
303
     * The keys returned are identical to the keys returned from PHP's
304
     * stream_get_meta_data() function.
305
     *
306
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
307
     * @param string|null $key Specific metadata to retrieve.
308
     * @return array|mixed|null Returns an associative array if no key is
309
     *     provided. Returns a specific key value if a key is provided and the
310
     *     value is found, or null if the key is not found.
311
     */
312 49
    public function getMetadata($key = null)
313
    {
314 49
        if (!$this->resource) {
315 1
            return $key ? null : [];
316
        }
317
318 48
        $metadata = stream_get_meta_data($this->resource);
319
320 48
        if ($key === null) {
321 1
            return $metadata;
322
        }
323
324 48
        if (array_key_exists($key, $metadata)) {
325 48
            return $metadata[$key];
326
        }
327
328
        return null;
329
    }
330
331
    /**
332
     * Initialization the stream resource.
333
     *
334
     * Called when creating `Psr\Http\Message\StreamInterface` instance.
335
     *
336
     * @param mixed $stream String stream target or stream resource.
337
     * @param string $mode Resource mode for stream target.
338
     * @throws RuntimeException if the stream or file cannot be opened.
339
     * @throws InvalidArgumentException if the stream or resource is invalid.
340
     */
341 318
    private function init($stream, string $mode): void
342
    {
343 318
        if (is_string($stream)) {
344 298
            $stream = $stream === '' ? false : @fopen($stream, $mode);
345
346 298
            if ($stream === false) {
347 6
                throw new RuntimeException('The stream or file cannot be opened.');
348
            }
349
        }
350
351 315
        if (!is_resource($stream) || get_resource_type($stream) !== 'stream') {
352
            throw new InvalidArgumentException(
353
                'Invalid stream provided. It must be a string stream identifier or stream resource.'
354
            );
355
        }
356
357 315
        $this->resource = $stream;
358 315
    }
359
}
360