Passed
Push — master ( 6c3352...d3ea2e )
by Evgeniy
02:50
created

StreamTrait::isSeekable()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

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