Passed
Pull Request — master (#29)
by Evgeniy
02:32
created

StreamTrait::getSize()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4

Importance

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