Stream::getContents()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 3.0416

Importance

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