StreamTrait   D
last analyzed

Complexity

Total Complexity 58

Size/Duplication

Total Lines 374
Duplicated Lines 0 %

Test Coverage

Coverage 90.91%

Importance

Changes 4
Bugs 0 Features 0
Metric Value
eloc 97
c 4
b 0
f 0
dl 0
loc 374
ccs 100
cts 110
cp 0.9091
rs 4.5599
wmc 58

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __toString() 0 7 2
A __destruct() 0 3 1
B isWritable() 0 16 7
A read() 0 15 4
A isSeekable() 0 7 3
A eof() 0 3 2
A close() 0 7 3
A init() 0 17 6
A tell() 0 11 3
A getSize() 0 12 4
A rewind() 0 3 1
A detach() 0 6 1
A seek() 0 12 4
A write() 0 17 4
A getMetadata() 0 13 4
A isReadable() 0 11 4
A getContents() 0 23 5

How to fix   Complexity   

Complex Class

Complex classes like StreamTrait often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StreamTrait, and based on these observations, apply Extract Interface, too.

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