Passed
Push — master ( ad65b5...642648 )
by Evgeniy
14:43
created

src/StreamTrait.php (4 issues)

Severity
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
    public function __destruct()
45
    {
46
        $this->close();
47 20
    }
48
49 20
    /**
50 20
     * 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
    public function __toString(): string
61
    {
62
        if ($this->isSeekable()) {
63 4
            $this->rewind();
64
        }
65 4
66 3
        return $this->getContents();
67
    }
68
69 4
    /**
70
     * Closes the stream and any underlying resources.
71
     *
72
     * @return void
73
     * @psalm-suppress PossiblyNullArgument
74
     */
75
    public function close(): void
76
    {
77
        if ($this->resource) {
78 26
            $resource = $this->detach();
79
            fclose($resource);
80 26
        }
81 25
    }
82 25
83
    /**
84 26
     * 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
    public function detach()
91
    {
92
        $resource = $this->resource;
93 26
        $this->resource = null;
94
        return $resource;
95 26
    }
96 26
97 26
    /**
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
    public function getSize(): ?int
103
    {
104
        if ($this->resource === null) {
105 3
            return null;
106
        }
107 3
108 2
        $stats = fstat($this->resource);
109
        return isset($stats['size']) ? (int) $stats['size'] : null;
110
    }
111 1
112 1
    /**
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
    public function tell(): int
119
    {
120
        if (!$this->resource) {
121 2
            throw new RuntimeException('No resource available. Cannot tell position');
122
        }
123 2
124 1
        if (!is_int($result = ftell($this->resource))) {
0 ignored issues
show
The condition is_int($result = ftell($this->resource)) is always true.
Loading history...
125
            throw new RuntimeException('Error occurred during tell operation');
126
        }
127 1
128
        return $result;
129
    }
130
131 1
    /**
132
     * Returns true if the stream is at the end of the stream.
133
     *
134
     * @return bool
135
     */
136
    public function eof(): bool
137
    {
138
        return (!$this->resource || feof($this->resource));
139 4
    }
140
141 4
    /**
142
     * Returns whether or not the stream is seekable.
143
     *
144
     * @return bool
145
     */
146
    public function isSeekable(): bool
147
    {
148
        return ($this->resource && $this->getMetadata('seekable'));
149 10
    }
150
151 10
    /**
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
    public function seek($offset, $whence = SEEK_SET): void
164
    {
165
        if (!$this->resource) {
166 7
            throw new RuntimeException('No resource available. Cannot seek position.');
167
        }
168 7
169 1
        if (!$this->isSeekable()) {
170
            throw new RuntimeException('Stream is not seekable.');
171
        }
172 6
173
        if (fseek($this->resource, $offset, $whence) !== 0) {
174
            throw new RuntimeException('Error seeking within stream.');
175
        }
176 6
    }
177
178
    /**
179 6
     * 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
    public function rewind(): void
189
    {
190
        $this->seek(0);
191 6
    }
192
193 6
    /**
194 6
     * Returns whether or not the stream is writable.
195
     *
196
     * @return bool
197
     * @psalm-suppress MixedAssignment
198
     */
199
    public function isWritable(): bool
200
    {
201
        if (!is_string($mode = $this->getMetadata('mode'))) {
202 7
            return false;
203
        }
204 7
205
        return (
206
            strpos($mode, 'w') !== false
207
            || strpos($mode, '+') !== false
208
            || strpos($mode, 'x') !== false
209 7
            || strpos($mode, 'c') !== false
210 7
            || strpos($mode, 'a') !== false
211 7
        );
212 7
    }
213 7
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
    public function write($string): int
222
    {
223
        if (!$this->resource) {
224 5
            throw new RuntimeException('No resource available. Cannot write.');
225
        }
226 5
227 1
        if (!$this->isWritable()) {
228
            throw new RuntimeException('Stream is not writable.');
229
        }
230 4
231
        if (!is_int($result = fwrite($this->resource, $string))) {
0 ignored issues
show
The condition is_int($result = fwrite(...is->resource, $string)) is always true.
Loading history...
232
            throw new RuntimeException('Error writing to stream.');
233
        }
234 4
235
        return $result;
236
    }
237
238 4
    /**
239
     * Returns whether or not the stream is readable.
240
     *
241
     * @return bool
242
     * @psalm-suppress MixedAssignment
243
     */
244
    public function isReadable(): bool
245
    {
246
        if (!is_string($mode = $this->getMetadata('mode'))) {
247 13
            return false;
248
        }
249 13
250 1
        return (strpos($mode, 'r') !== false || strpos($mode, '+') !== false);
251
    }
252
253 12
    /**
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
    public function read($length): string
264
    {
265
        if (!$this->resource) {
266 4
            throw new RuntimeException('No resource available. Cannot read.');
267
        }
268 4
269 1
        if (!$this->isReadable()) {
270
            throw new RuntimeException('Stream is not readable.');
271
        }
272 3
273
        if (!is_string($result = fread($this->resource, $length))) {
0 ignored issues
show
The condition is_string($result = frea...is->resource, $length)) is always true.
Loading history...
274
            throw new RuntimeException('Error reading stream.');
275
        }
276 3
277
        return $result;
278
    }
279
280 3
    /**
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
    public function getContents(): string
288
    {
289
        if (!$this->isReadable()) {
290 9
            throw new RuntimeException('Stream is not readable.');
291
        }
292 9
293 2
        if (!is_string($result = stream_get_contents($this->resource))) {
0 ignored issues
show
The condition is_string($result = stre...tents($this->resource)) is always true.
Loading history...
294
            throw new RuntimeException('Error reading stream.');
295
        }
296 7
297
        return $result;
298
    }
299
300 7
    /**
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
    public function getMetadata($key = null)
313
    {
314
        if (!$this->resource) {
315 21
            return $key ? null : [];
316
        }
317 21
318 1
        $metadata = stream_get_meta_data($this->resource);
319
320
        if ($key === null) {
321 20
            return $metadata;
322
        }
323 20
324 1
        if (array_key_exists($key, $metadata)) {
325
            return $metadata[$key];
326
        }
327 20
328 20
        return null;
329
    }
330
331
    /**
332
     * Initialization the stream resource.
333
     *
334
     * Called when creating `Psr\Http\Message\StreamInterface` instance.
335
     *
336
     * @param string|resource $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
    private function init($stream, string $mode): void
342
    {
343
        if (is_string($stream)) {
344 201
            $stream = @fopen($stream, $mode);
345
346 201
            if ($stream === false) {
347 200
                throw new RuntimeException('The stream or file cannot be opened.');
348 4
            }
349 4
        }
350
351
        if (!is_resource($stream) || get_resource_type($stream) !== 'stream') {
352 200
            throw new InvalidArgumentException(
353
                'Invalid stream provided. It must be a string stream identifier or stream resource.'
354
            );
355 200
        }
356 198
357 200
        $this->resource = $stream;
358
    }
359
}
360