Passed
Push — master ( 6d8644...3a85d2 )
by Evgeniy
02:32
created

StreamTrait::init()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 17
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 6

Importance

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