StreamTrait::tell()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

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