Passed
Push — master ( e41252...69cd60 )
by Evgeniy
01:59
created

src/StreamTrait.php (4 issues)

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