AbstractStream::isWritable()   A
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 13
rs 9.2222
cc 6
nc 6
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * This file is part of slick/http
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Slick\Http\Message\Stream;
13
14
use Psr\Http\Message\StreamInterface;
15
use Slick\Http\Message\Exception\RuntimeException;
16
17
/**
18
 * Class AbstractStream
19
 * @package Slick\Http\Message\Stream
20
 */
21
abstract class AbstractStream implements StreamInterface
22
{
23
    /**
24
     * @var null|resource
25
     */
26
    protected mixed $stream = null;
27
28
    /**
29
     * Reads all data from the stream into a string, from the beginning to end.
30
     *
31
     * This method MUST attempt to seek to the beginning of the stream before
32
     * reading data and read the stream until the end is reached.
33
     *
34
     * Warning: This could attempt to load a large amount of data into memory.
35
     *
36
     * This method MUST NOT raise an exception in order to conform with PHP's
37
     * string casting operations.
38
     *
39
     * @see http://php.net/manual/en/language.oop5.magic.php#object.tostring
40
     * @return string
41
     */
42
    public function __toString(): string
43
    {
44
        if (!$this->isReadable()) {
45
            return '';
46
        }
47
        $value = '';
48
        try {
49
            $this->rewind();
50
            $value = $this->getContents();
51
        } catch (\RuntimeException) {
0 ignored issues
show
Coding Style Comprehensibility introduced by
Consider adding a comment why this CATCH block is empty.
Loading history...
52
        }
53
        return (string) $value;
54
    }
55
56
    /**
57
     * Closes the stream and any underlying resources.
58
     *
59
     * @return void
60
     */
61
    public function close(): void
62
    {
63
        if (\is_resource($this->stream)) {
64
            fclose($this->stream);
65
        }
66
    }
67
68
    /**
69
     * Separates any underlying resources from the stream.
70
     *
71
     * After the stream has been detached, the stream is in an unusable state.
72
     *
73
     * @return resource|null Underlying PHP stream, if any
74
     */
75
    public function detach(): mixed
76
    {
77
        $resource = $this->stream;
78
        $this->stream = null;
79
        return $resource;
80
    }
81
82
    /**
83
     * Get the size of the stream if known.
84
     *
85
     * @return int|null Returns the size in bytes if known, or null if unknown.
86
     */
87
    public function getSize(): ?int
88
    {
89
        if ($this->stream === null) {
90
            return null;
91
        }
92
93
        $stats = fstat($this->stream);
94
        if ($stats === false) {
95
            return null;
96
        }
97
98
        return $stats['size'];
99
    }
100
101
    /**
102
     * Returns the current position of the file read/write pointer
103
     *
104
     * @return int Position of the file pointer
105
     * @throws RuntimeException on error.
106
     */
107
    public function tell(): int
108
    {
109
        if (!$this->stream) {
110
            throw new RuntimeException(
111
                'No resource available; cannot tell position'
112
            );
113
        }
114
        $result = ftell($this->stream);
115
        if (!\is_int($result)) {
0 ignored issues
show
introduced by
The condition is_int($result) is always true.
Loading history...
116
            throw new RuntimeException(
117
                'Error occurred during tell operation'
118
            );
119
        }
120
        return $result;
121
    }
122
123
    /**
124
     * Returns true if the stream is at the end of the stream.
125
     *
126
     * @return bool
127
     */
128
    public function eof(): bool
129
    {
130
        $return = true;
131
        if (\is_resource($this->stream)) {
132
            $return = feof($this->stream);
133
        }
134
        return $return;
135
    }
136
137
    /**
138
     * Returns whether the stream is seekable.
139
     *
140
     * @return bool
141
     */
142
    public function isSeekable(): bool
143
    {
144
        $seekable = false;
145
        if ($this->stream) {
146
            $meta = stream_get_meta_data($this->stream);
147
            $seekable = $meta['seekable'];
148
        }
149
        return $seekable;
150
    }
151
152
    /**
153
     * Seek to a position in the stream.
154
     *
155
     * @link http://www.php.net/manual/en/function.fseek.php
156
     * @param int $offset Stream offset
157
     * @param int $whence Specifies how the cursor position will be calculated
158
     *     based on the seek offset. Valid values are identical to the built-in
159
     *     PHP $whence values for `fseek()`.  SEEK_SET: Set position equal to
160
     *     offset bytes SEEK_CUR: Set position to current location plus offset
161
     *     SEEK_END: Set position to end-of-stream plus offset.
162
     * @throws RuntimeException on failure.
163
     *
164
     * @return void
165
     */
166
    public function seek(int $offset, int $whence = SEEK_SET): void
167
    {
168
        if (! $this->stream) {
169
            throw new RuntimeException('No resource available; cannot seek position');
170
        }
171
        if (! $this->isSeekable()) {
172
            throw new RuntimeException('Stream is not seekable');
173
        }
174
        $result = fseek($this->stream, $offset, $whence);
175
        if (0 !== $result) {
176
            throw new RuntimeException('Error seeking within stream');
177
        }
178
    }
179
180
    /**
181
     * Seek to the beginning of the stream.
182
     *
183
     * If the stream is not seekable, this method will raise an exception;
184
     * otherwise, it will perform a seek(0).
185
     *
186
     * @see seek()
187
     * @link http://www.php.net/manual/en/function.fseek.php
188
     * @throws \RuntimeException on failure.
189
     */
190
    public function rewind(): void
191
    {
192
        $this->seek(0);
193
    }
194
195
    /**
196
     * Returns whether the stream is writable.
197
     *
198
     * @return bool
199
     */
200
    public function isWritable(): bool
201
    {
202
        if (! $this->stream) {
203
            return false;
204
        }
205
        $meta = stream_get_meta_data($this->stream);
206
        $mode = $meta['mode'];
207
        return (
208
            strstr($mode, 'x')
209
            || strstr($mode, 'w')
210
            || strstr($mode, 'c')
211
            || strstr($mode, 'a')
212
            || strstr($mode, '+')
213
        );
214
    }
215
216
    /**
217
     * Write data to the stream.
218
     *
219
     * @param string $string The string that is to be written.
220
     * @return int Returns the number of bytes written to the stream.
221
     * @throws RuntimeException on failure.
222
     */
223
    public function write(string $string): int
224
    {
225
        if (! $this->stream) {
226
            throw new RuntimeException('No resource available; cannot write');
227
        }
228
        if (! $this->isWritable()) {
229
            throw new RuntimeException('Stream is not writable');
230
        }
231
        $result = fwrite($this->stream, $string);
232
        if (false === $result) {
233
            throw new RuntimeException('Error writing to stream');
234
        }
235
        return $result;
236
    }
237
238
    /**
239
     * Returns whether the stream is readable.
240
     *
241
     * @return bool
242
     */
243
    public function isReadable(): bool
244
    {
245
        if (! $this->stream) {
246
            return false;
247
        }
248
        $meta = stream_get_meta_data($this->stream);
249
        $mode = $meta['mode'];
250
        return (strstr($mode, 'r') || strstr($mode, '+'));
251
    }
252
253
    /**
254
     * Read data from the stream.
255
     *
256
     * @param int<1, max> $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(int $length): string
264
    {
265
        if (! $this->stream) {
266
            throw new RuntimeException('No resource available; cannot read');
267
        }
268
        if (! $this->isReadable()) {
269
            throw new RuntimeException('Stream is not readable');
270
        }
271
        $result = fread($this->stream, $length);
272
        if (false === $result) {
273
            throw new RuntimeException('Error reading stream');
274
        }
275
        return $result;
276
    }
277
278
    /**
279
     * Returns the remaining contents in a string
280
     *
281
     * @return string
282
     * @throws RuntimeException if unable to read or an error occurs while
283
     *     reading.
284
     */
285
    public function getContents(): string
286
    {
287
        if (! $this->isReadable()) {
288
            throw new RuntimeException('Stream is not readable');
289
        }
290
        $result = stream_get_contents($this->stream);
291
        if (false === $result) {
292
            throw new RuntimeException('Error reading from stream');
293
        }
294
        return $result;
295
    }
296
297
    /**
298
     * Get stream metadata as an associative array or retrieve a specific key.
299
     *
300
     * The keys returned are identical to the keys returned from PHP's
301
     * stream_get_meta_data() function.
302
     *
303
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
304
     * @param string $key Specific metadata to retrieve.
305
     * @return array|mixed|null Returns an associative array if no key is
306
     *     provided. Returns a specific key value if a key is provided and the
307
     *     value is found, or null if the key is not found.
308
     */
309
    public function getMetadata($key = null): mixed
310
    {
311
        if (null === $key) {
312
            return stream_get_meta_data($this->stream);
313
        }
314
        $metadata = stream_get_meta_data($this->stream);
315
        if (! \array_key_exists($key, $metadata)) {
316
            return null;
317
        }
318
        return $metadata[$key];
319
    }
320
321
    /**
322
     * Closes the stream on destruction
323
     */
324
    public function __destruct()
325
    {
326
        $this->close();
327
    }
328
}
329