Passed
Push — master ( 69b17c...082853 )
by Terry
02:44
created

Stream::seek()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 20
CRAP Score 3

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 17
c 5
b 0
f 0
dl 0
loc 27
ccs 20
cts 20
cp 1
rs 9.7
cc 3
nc 3
nop 2
crap 3
1
<?php 
2
/**
3
 * This file is part of the Shieldon package.
4
 *
5
 * (c) Terry L. <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
declare(strict_types=1);
12
13
namespace Shieldon\Psr7;
14
15
use Psr\Http\Message\StreamInterface;
16
use InvalidArgumentException;
17
use RuntimeException;
18
19
use function fclose;
20
use function fread;
21
use function fseek;
22
use function fstat;
23
use function ftell;
24
use function fwrite;
25
use function gettype;
26
use function is_resource;
27
use function preg_match;
28
use function sprintf;
29
use function stream_get_contents;
30
use function stream_get_meta_data;
31
32
use const SEEK_CUR;
33
use const SEEK_END;
34
use const SEEK_SET;
35
use const PREG_OFFSET_CAPTURE;
36
37
/*
38
 * Describes a data stream.
39
 */
40
class Stream implements StreamInterface
41
{
42
    /**
43
     * @var bool
44
     */
45
    protected $readable;
46
47
    /**
48
     * @var bool
49
     */
50
    protected $writable;
51
52
    /**
53
     * @var bool
54
     */
55
    protected $seekable;
56
57
    /**
58
     * The size of the stream.
59
     *
60
     * @var int|null
61
     */
62
    protected $size;
63
64
    /**
65
     * The keys returned are identical to the keys returned from PHP's
66
     * stream_get_meta_data() function.
67
     *
68
     * @var array
69
     */
70
    protected $meta;
71
72
    /**
73
     * Typically a PHP resource.
74
     *
75
     * @var resource
76
     */
77
    protected $stream;
78
79
    /**
80
     * Stream constructor
81
     * 
82
     * @param resource $stream A valid resource.
83
     */
84 51
    public function __construct($stream)
85
    {
86 51
        $this->assertStream($stream);
87 50
        $this->stream = $stream;
88
89 50
        $meta = $this->getMetadata();
90
91 50
        $this->readable = false;
92 50
        $this->writable = false;
93
94
        // The mode parameter specifies the type of access you require to 
95
        // the stream. @see https://www.php.net/manual/en/function.fopen.php
96 50
        if (strpos($meta['mode'], '+') !== false) {
97 44
            $this->readable = true;
98 44
            $this->writable = true;
99
        }
100
101 50
        if (preg_match('/^[waxc][t|b]{0,1}$/', $meta['mode'], $matches, PREG_OFFSET_CAPTURE)) {
102 1
            $this->writable = true;
103
        }
104
105 50
        if (strpos($meta['mode'], 'r') !== false) {
106 14
            $this->readable = true;
107
        }
108
109 50
        $this->seekable = $meta['seekable'];
110
    }
111
112
    /**
113
     * {@inheritdoc}
114
     */
115 7
    public function isWritable(): bool
116
    {
117 7
        return $this->writable;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 12
    public function isReadable(): bool
124
    {
125 12
        return $this->readable;
126
    }
127
128
    /**
129
     * {@inheritdoc}
130
     */
131 3
    public function isSeekable(): bool
132
    {
133 3
        return $this->seekable;
134
    }
135
136
    /**
137
     * {@inheritdoc}
138
     */
139 9
    public function close(): void
140
    {
141 9
        if ($this->isStream()) {
142 9
            fclose($this->stream);
143
        }
144
145 9
        $this->detach();
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 10
    public function detach()
152
    {
153 10
        if (!$this->isStream()) {
154 10
            return null;
155
        }
156
157 1
        $legacy = $this->stream;
158
        
159 1
        $this->readable = false;
160 1
        $this->writable = false;
161 1
        $this->seekable = false;
162 1
        $this->size = null;
163 1
        $this->meta = [];
164
165 1
        unset($this->stream);
166
167 1
        return $legacy;
168
    }
169
170
    /**
171
     * {@inheritdoc}
172
     */
173 3
    public function getSize(): ?int
174
    {
175 3
        if (!$this->isStream()) {
176 1
            return null;
177
        }
178
179 3
        if ($this->size === null) {
180 3
            $stats = fstat($this->stream);
181 3
            $this->size = $stats['size'] ?? null;
182
        }
183
184 3
        return $this->size;
185
    }
186
187
    /**
188
     * {@inheritdoc}
189
     */
190 3
    public function tell(): int
191
    {
192 3
        $this->assertPropertyStream();
193
194 2
        $pointer = false;
195
196 2
        if ($this->stream) {
197 2
            $pointer = ftell($this->stream);
198
        }
199
200 2
        if ($pointer === false) {
201
202
            // @codeCoverageIgnoreStart
203
204
            throw new RuntimeException(
205
                'Unable to get the position of the file pointer in stream.'
206
            );
207
208
            // @codeCoverageIgnoreEnd
209
        }
210
211 2
        return $pointer;
212
    }
213
214
    /**
215
     * {@inheritdoc}
216
     */
217 1
    public function eof(): bool
218
    {
219 1
        return $this->stream ? feof($this->stream) : true;
220
    }
221
222
    /**
223
     * {@inheritdoc}
224
     */
225 11
    public function seek($offset, $whence = SEEK_SET): void
226
    {
227 11
        $this->assertPropertyStream();
228
229 10
        if (!$this->seekable) {
230 1
            throw new RuntimeException(
231 1
                'Stream is not seekable.'
232 1
            );
233
        }
234
235 9
        $offset = (int) $offset;
236 9
        $whence = (int) $whence;
237
238 9
        $message = [
239 9
            SEEK_CUR => 'Set position to current location plus offset.',
240 9
            SEEK_END => 'Set position to end-of-stream plus offset.',
241 9
            SEEK_SET => 'Set position equal to offset bytes.',
242 9
        ];
243
244 9
        $errorMsg = $message[$whence] ?? 'Unknown error.';
245
246 9
        if (fseek($this->stream, $offset, $whence) === -1) {
247 1
            throw new RuntimeException(
248 1
                sprintf(
249 1
                    '%s. Unable to seek to stream at position %s',
250 1
                    $errorMsg,
251 1
                    $offset
252 1
                )
253 1
            );
254
        }
255
    }
256
257
    /**
258
     * {@inheritdoc}
259
     */
260 8
    public function rewind(): void
261
    {
262 8
        $this->seek(0);
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268 7
    public function write($string): int
269
    {
270 7
        $this->assertPropertyStream();
271
272 6
        $size = 0;
273
274 6
        if ($this->isWritable()) {
275 6
            $size = fwrite($this->stream, $string);
276
        }
277
278 6
        if ($size === false) {
0 ignored issues
show
introduced by
The condition $size === false is always false.
Loading history...
279
280
            // @codeCoverageIgnoreStart
281
282
            throw new RuntimeException(
283
                'Unable to write to stream.'
284
            );
285
286
            // @codeCoverageIgnoreEnd
287
        }
288
289
        // Make sure that `getSize()`will count the correct size again after writing anything.
290 6
        $this->size = null;
291
292 6
        return $size;
293
    }
294
295
    /**
296
     * {@inheritdoc}
297
     */
298 2
    public function read($length): string
299
    {
300 2
        $this->assertPropertyStream();
301
302 1
        $string = false;
303
304 1
        if ($this->isReadable()) {
305 1
            $string = fread($this->stream, $length);
306
        }
307
308 1
        if ($string === false) {
309
310
            // @codeCoverageIgnoreStart
311
312
            throw new RuntimeException(
313
                'Unable to read from stream.'
314
            );
315
316
            // @codeCoverageIgnoreEnd
317
        }
318
319 1
        return $string;
320
    }
321
322
    /**
323
     * {@inheritdoc}
324
     */
325 10
    public function getContents(): string
326
    {
327 10
        $this->assertPropertyStream();
328
329 9
        $string = false;
330
331 9
        if ($this->isReadable()) {
332 8
            $string = stream_get_contents($this->stream);
333
        }
334
335 9
        if ($string === false) {
336 1
            throw new RuntimeException(
337 1
                'Unable to read stream contents.'
338 1
            );
339
        }
340
341 8
        return $string;
342
    }
343
344
    /**
345
     * {@inheritdoc}
346
     */
347 50
    public function getMetadata($key = null)
348
    {
349 50
        if ($this->isStream()) {
350 50
            $this->meta = stream_get_meta_data($this->stream);
351
            
352 50
            if (!$key) {
353 50
                return $this->meta;
354
            }
355
356 1
            if (isset($this->meta[$key])) {
357 1
                return $this->meta[$key];
358
            }
359
        }
360
361 1
        return null;
362
    }
363
364
    /**
365
     * {@inheritdoc}
366
     */
367 2
    public function __toString(): string
368
    {
369 2
        if ($this->isSeekable()) {
370 2
            $this->rewind();
371
        }
372
373 2
        return $this->getContents();
374
    }
375
376
    /*
377
    |--------------------------------------------------------------------------
378
    | Non PSR-7 Methods.
379
    |--------------------------------------------------------------------------
380
    */
381
382
    /**
383
     * Throw exception if stream is not a valid PHP resource.
384
     *
385
     * @param resource $stream A valid resource.
386
     * 
387
     * @return void
388
     * 
389
     * InvalidArgumentException
390
     */
391 51
    protected function assertStream($stream): void
392
    {
393 51
        if (!is_resource($stream)) {
394 1
            throw new InvalidArgumentException(
395 1
                sprintf(
396 1
                    'Stream should be a resource, but "%s" provided.',
397 1
                    gettype($stream)
398 1
                )
399 1
            );
400
        }
401
    }
402
403
    /**
404
     * Throw an exception if the property does not exist.
405
     *
406
     * @return RuntimeException
407
     */
408 19
    protected function assertPropertyStream(): void
409
    {
410 19
        if (!$this->isStream()) {
411 5
            throw new RuntimeException(
412 5
                'Stream does not exist.'
413 5
            );
414
        }
415
    }
416
417
    /**
418
     * Check if stream exists or not.
419
     *
420
     * @return bool
421
     */
422 50
    protected function isStream(): bool
423
    {
424 50
        return (isset($this->stream) && is_resource($this->stream));
425
    }
426
}
427