Completed
Branch master (386b65)
by Terry
01:47
created

Stream::isStream()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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