Passed
Pull Request — master (#27)
by Anatoly
39:10
created

Stream::seek()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 13
ccs 8
cts 8
cp 1
crap 4
rs 10
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message;
13
14
/**
15
 * Import classes
16
 */
17
use Psr\Http\Message\StreamInterface;
18
use Sunrise\Http\Message\Exception\FailedStreamOperationException;
19
use Sunrise\Http\Message\Exception\InvalidArgumentException;
20
use Sunrise\Http\Message\Exception\InvalidStreamException;
21
use Sunrise\Http\Message\Exception\InvalidStreamOperationException;
22
use Throwable;
23
24
/**
25
 * Import functions
26
 */
27
use function fclose;
28
use function feof;
29
use function fread;
30
use function fseek;
31
use function fstat;
32
use function ftell;
33
use function fwrite;
34
use function is_resource;
35
use function stream_get_contents;
36
use function stream_get_meta_data;
37
use function strpbrk;
38
39
/**
40
 * Import constants
41
 */
42
use const SEEK_SET;
43
44
/**
45
 * Stream
46
 *
47
 * @link https://www.php-fig.org/psr/psr-7/
48
 */
49
class Stream implements StreamInterface
50
{
51
52
    /**
53
     * The stream resource
54
     *
55
     * @var resource|null
56
     */
57
    private $resource;
58
59
    /**
60
     * Signals to close the stream on destruction
61
     *
62
     * @var bool
63
     */
64
    private $autoClose;
65
66
    /**
67
     * Constructor of the class
68
     *
69
     * @param mixed $resource
70
     * @param bool $autoClose
71
     *
72
     * @throws InvalidArgumentException
73
     *         If the stream cannot be initialized with the resource.
74
     */
75 174
    public function __construct($resource, bool $autoClose = true)
76
    {
77 174
        if (!is_resource($resource)) {
78 4
            throw new InvalidArgumentException('Unexpected stream resource');
79
        }
80
81 173
        $this->resource = $resource;
82 173
        $this->autoClose = $autoClose;
83
    }
84
85
    /**
86
     * Creates a stream
87
     *
88
     * @param mixed $resource
89
     *
90
     * @return StreamInterface
91
     *
92
     * @throws InvalidArgumentException
93
     *         If a stream cannot be created.
94
     */
95 3
    public static function create($resource): StreamInterface
96
    {
97 3
        if ($resource instanceof StreamInterface) {
98 1
            return $resource;
99
        }
100
101 2
        return new self($resource);
102
    }
103
104
    /**
105
     * Destructor of the class
106
     */
107 124
    public function __destruct()
108
    {
109 124
        if ($this->autoClose) {
110 113
            $this->close();
111
        }
112
    }
113
114
    /**
115
     * Detaches a resource from the stream
116
     *
117
     * Returns NULL if the stream already without a resource.
118
     *
119
     * @return resource|null
120
     */
121 173
    public function detach()
122
    {
123 173
        $resource = $this->resource;
124 173
        $this->resource = null;
125
126 173
        return $resource;
127
    }
128
129
    /**
130
     * Closes the stream
131
     *
132
     * @link http://php.net/manual/en/function.fclose.php
133
     *
134
     * @return void
135
     */
136 173
    public function close(): void
137
    {
138 173
        $resource = $this->detach();
139 173
        if (!is_resource($resource)) {
140 40
            return;
141
        }
142
143 156
        fclose($resource);
144
    }
145
146
    /**
147
     * Checks if the end of the stream is reached
148
     *
149
     * @link http://php.net/manual/en/function.feof.php
150
     *
151
     * @return bool
152
     */
153 11
    public function eof(): bool
154
    {
155 11
        if (!is_resource($this->resource)) {
156 2
            return true;
157
        }
158
159 9
        return feof($this->resource);
160
    }
161
162
    /**
163
     * Gets the stream pointer position
164
     *
165
     * @link http://php.net/manual/en/function.ftell.php
166
     *
167
     * @return int
168
     *
169
     * @throws InvalidStreamException
170
     * @throws FailedStreamOperationException
171
     */
172 8
    public function tell(): int
173
    {
174 8
        if (!is_resource($this->resource)) {
175 2
            throw InvalidStreamException::noResource();
176
        }
177
178 6
        $result = ftell($this->resource);
179 6
        if ($result === false) {
180 1
            throw new FailedStreamOperationException('Unable to get the stream pointer position');
181
        }
182
183 5
        return $result;
184
    }
185
186
    /**
187
     * Checks if the stream is seekable
188
     *
189
     * @return bool
190
     */
191 83
    public function isSeekable(): bool
192
    {
193 83
        if (!is_resource($this->resource)) {
194 2
            return false;
195
        }
196
197
        /** @var array{seekable: bool} */
198 81
        $metadata = stream_get_meta_data($this->resource);
199
200 81
        return $metadata['seekable'];
201
    }
202
203
    /**
204
     * Moves the stream pointer to the beginning
205
     *
206
     * @return void
207
     *
208
     * @throws InvalidStreamException
209
     * @throws InvalidStreamOperationException
210
     * @throws FailedStreamOperationException
211
     */
212 70
    public function rewind(): void
213
    {
214 70
        $this->seek(0);
215
    }
216
217
    /**
218
     * Moves the stream pointer to the given position
219
     *
220
     * @link http://php.net/manual/en/function.fseek.php
221
     *
222
     * @param int $offset
223
     * @param int $whence
224
     *
225
     * @return void
226
     *
227
     * @throws InvalidStreamException
228
     * @throws InvalidStreamOperationException
229
     * @throws FailedStreamOperationException
230
     */
231 78
    public function seek($offset, $whence = SEEK_SET): void
232
    {
233 78
        if (!is_resource($this->resource)) {
234 4
            throw InvalidStreamException::noResource();
235
        }
236
237 74
        if (!$this->isSeekable()) {
238 3
            throw new InvalidStreamOperationException('Stream is not seekable');
239
        }
240
241 71
        $result = fseek($this->resource, $offset, $whence);
242 71
        if ($result !== 0) {
243 1
            throw new FailedStreamOperationException('Unable to move the stream pointer position');
244
        }
245
    }
246
247
    /**
248
     * Checks if the stream is writable
249
     *
250
     * @return bool
251
     */
252 37
    public function isWritable(): bool
253
    {
254 37
        if (!is_resource($this->resource)) {
255 2
            return false;
256
        }
257
258
        /** @var array{mode: string} */
259 35
        $metadata = stream_get_meta_data($this->resource);
260
261 35
        return strpbrk($metadata['mode'], '+acwx') !== false;
262
    }
263
264
    /**
265
     * Writes the given string to the stream
266
     *
267
     * Returns the number of bytes written to the stream.
268
     *
269
     * @link http://php.net/manual/en/function.fwrite.php
270
     *
271
     * @param string $string
272
     *
273
     * @return int
274
     *
275
     * @throws InvalidStreamException
276
     * @throws InvalidStreamOperationException
277
     * @throws FailedStreamOperationException
278
     */
279 27
    public function write($string): int
280
    {
281 27
        if (!is_resource($this->resource)) {
282 2
            throw InvalidStreamException::noResource();
283
        }
284
285 25
        if (!$this->isWritable()) {
286 1
            throw new InvalidStreamOperationException('Stream is not writable');
287
        }
288
289 24
        $result = fwrite($this->resource, $string);
290 24
        if ($result === false) {
291
            throw new FailedStreamOperationException('Unable to write to the stream');
292
        }
293
294 24
        return $result;
295
    }
296
297
    /**
298
     * Checks if the stream is readable
299
     *
300
     * @return bool
301
     */
302 42
    public function isReadable(): bool
303
    {
304 42
        if (!is_resource($this->resource)) {
305 4
            return false;
306
        }
307
308
        /** @var array{mode: string} */
309 38
        $metadata = stream_get_meta_data($this->resource);
310
311 38
        return strpbrk($metadata['mode'], '+r') !== false;
312
    }
313
314
    /**
315
     * Reads the given number of bytes from the stream
316
     *
317
     * @link http://php.net/manual/en/function.fread.php
318
     *
319
     * @param int $length
320
     *
321
     * @return string
322
     *
323
     * @throws InvalidStreamException
324
     * @throws InvalidStreamOperationException
325
     * @throws FailedStreamOperationException
326
     */
327 15
    public function read($length): string
328
    {
329 15
        if (!is_resource($this->resource)) {
330 2
            throw InvalidStreamException::noResource();
331
        }
332
333 13
        if (!$this->isReadable()) {
334 1
            throw new InvalidStreamOperationException('Stream is not readable');
335
        }
336
337 12
        $result = fread($this->resource, $length);
338 12
        if ($result === false) {
339
            throw new FailedStreamOperationException('Unable to read from the stream');
340
        }
341
342 12
        return $result;
343
    }
344
345
    /**
346
     * Reads the remainder of the stream
347
     *
348
     * @link http://php.net/manual/en/function.stream-get-contents.php
349
     *
350
     * @return string
351
     *
352
     * @throws InvalidStreamException
353
     * @throws InvalidStreamOperationException
354
     * @throws FailedStreamOperationException
355
     */
356 14
    public function getContents(): string
357
    {
358 14
        if (!is_resource($this->resource)) {
359 2
            throw InvalidStreamException::noResource();
360
        }
361
362 12
        if (!$this->isReadable()) {
363 1
            throw new InvalidStreamOperationException('Stream is not readable');
364
        }
365
366 11
        $result = stream_get_contents($this->resource);
367 11
        if ($result === false) {
368
            throw new FailedStreamOperationException('Unable to read the remainder of the stream');
369
        }
370
371 11
        return $result;
372
    }
373
374
    /**
375
     * Gets the stream metadata
376
     *
377
     * @link http://php.net/manual/en/function.stream-get-meta-data.php
378
     *
379
     * @param string|null $key
380
     *
381
     * @return mixed
382
     */
383 23
    public function getMetadata($key = null)
384
    {
385 23
        if (!is_resource($this->resource)) {
386 2
            return null;
387
        }
388
389 21
        $metadata = stream_get_meta_data($this->resource);
390 21
        if ($key === null) {
391 1
            return $metadata;
392
        }
393
394 20
        return $metadata[$key] ?? null;
395
    }
396
397
    /**
398
     * Gets the stream size
399
     *
400
     * Returns NULL if the stream without a resource,
401
     * or if the stream size cannot be determined.
402
     *
403
     * @link http://php.net/manual/en/function.fstat.php
404
     *
405
     * @return int|null
406
     */
407 7
    public function getSize(): ?int
408
    {
409 7
        if (!is_resource($this->resource)) {
410 2
            return null;
411
        }
412
413
        /** @var array{size: int}|false */
414 5
        $stats = fstat($this->resource);
415 5
        if ($stats === false) {
416
            return null;
417
        }
418
419 5
        return $stats['size'];
420
    }
421
422
    /**
423
     * Converts the stream to a string
424
     *
425
     * @link http://php.net/manual/en/language.oop5.magic.php#object.tostring
426
     *
427
     * @return string
428
     */
429 11
    public function __toString(): string
430
    {
431 11
        if (!$this->isReadable()) {
432 3
            return '';
433
        }
434
435
        try {
436 8
            if ($this->isSeekable()) {
437 8
                $this->rewind();
438
            }
439
440 8
            return $this->getContents();
441
        } catch (Throwable $e) {
442
            return '';
443
        }
444
    }
445
}
446