Passed
Push — master ( 0d4e0e...e71c95 )
by Evgeniy
02:20
created

StreamTrait::isWritable()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 16
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 7.0368

Importance

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