Passed
Branch master (7c9779)
by Terry
01:38
created

Stream::assertPropertyStream()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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