Passed
Push — master ( 62cb71...c30d3b )
by Evgeniy
02:30 queued 52s
created

StreamTrait   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Test Coverage

Coverage 90.43%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 75
c 1
b 0
f 0
dl 0
loc 324
ccs 85
cts 94
cp 0.9043
rs 7.44
wmc 52

17 Methods

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

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