Stream::isSeekable()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
rs 10
1
<?php
2
3
/**
4
 * Platine HTTP
5
 *
6
 * Platine HTTP Message is the implementation of PSR 7
7
 *
8
 * This content is released under the MIT License (MIT)
9
 *
10
 * Copyright (c) 2020 Platine HTTP
11
 * Copyright (c) 2019 Dion Chaika
12
 *
13
 * Permission is hereby granted, free of charge, to any person obtaining a copy
14
 * of this software and associated documentation files (the "Software"), to deal
15
 * in the Software without restriction, including without limitation the rights
16
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
 * copies of the Software, and to permit persons to whom the Software is
18
 * furnished to do so, subject to the following conditions:
19
 *
20
 * The above copyright notice and this permission notice shall be included in all
21
 * copies or substantial portions of the Software.
22
 *
23
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
 * SOFTWARE.
30
 */
31
32
/**
33
 *  @file Stream.php
34
 *
35
 *  The Default or base Stream class
36
 *
37
 *  @package    Platine\Http
38
 *  @author Platine Developers Team
39
 *  @copyright  Copyright (c) 2020
40
 *  @license    http://opensource.org/licenses/MIT  MIT License
41
 *  @link   https://www.platine-php.com
42
 *  @version 1.0.0
43
 *  @filesource
44
 */
45
46
declare(strict_types=1);
47
48
namespace Platine\Http;
49
50
use Exception;
51
use InvalidArgumentException;
52
use RuntimeException;
53
54
/**
55
 * @class Stream
56
 * @package Platine\Http
57
 */
58
class Stream implements StreamInterface
59
{
60
    /**
61
     * The writable stream modes.
62
     */
63
    protected const MODES_WRITE = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
64
65
    /**
66
     * The readable stream modes.
67
     */
68
    protected const MODES_READ = ['r', 'r+', 'w+', 'a+', 'x+', 'c+'];
69
70
    /**
71
     * The stream resource
72
     * @var resource|null
73
     */
74
    protected $resource;
75
76
    /**
77
     * The stream size
78
     * @var int|null
79
     */
80
    protected ?int $size;
81
82
    /**
83
     * Whether the stream is seekable
84
     * @var boolean
85
     */
86
    protected bool $seekable = false;
87
88
    /**
89
     * Whether the stream is writable
90
     * @var boolean
91
     */
92
    protected bool $writable = false;
93
94
    /**
95
     * Whether the stream is readable
96
     * @var boolean
97
     */
98
    protected bool $readable = false;
99
100
    /**
101
     * Create new Stream
102
     * @param string|resource $content the filename or resource instance
103
     * @param string $mode    the stream mode
104
     * @param array<string, mixed>  $options the stream options
105
     */
106
    public function __construct(
107
        $content = '',
108
        string $mode = 'r+',
109
        array $options = []
110
    ) {
111
        if (is_string($content)) {
112
            if (is_file($content) || strpos($content, 'php://') === 0) {
113
                $mode = $this->filterMode($mode);
114
                $resource = fopen($content, $mode);
115
                if ($resource === false) {
116
                    throw new RuntimeException(sprintf(
117
                        'Unable to create a stream from file [%s] !',
118
                        $content
119
                    ));
120
                }
121
                $this->resource = $resource;
122
            } else {
123
                $resource = fopen('php://temp', 'r+');
124
                if ($resource === false || fwrite($resource, $content) === false) {
125
                    throw new RuntimeException(
126
                        'Unable to create a stream from string'
127
                    );
128
                }
129
                $this->resource = $resource;
130
            }
131
        } elseif (is_resource($content)) {
132
            $this->resource = $content;
133
        } else {
134
            throw new InvalidArgumentException(
135
                'Stream resource must be valid PHP resource'
136
            );
137
        }
138
139
        if (isset($options['size']) && is_int($options['size']) && $options['size'] >= 0) {
140
            $this->size = $options['size'];
141
        } else {
142
            $fstat = fstat($this->resource);
143
            if ($fstat === false) {
144
                $this->size = null;
145
            } else {
146
                $this->size = !empty($fstat['size']) ? $fstat['size'] : null;
147
            }
148
        }
149
150
        $meta = stream_get_meta_data($this->resource);
151
        $this->seekable = isset($options['seekable'])
152
                            && is_bool($options['seekable'])
153
                                ? $options['seekable']
154
                                : (!empty($meta['seekable'])
155
                                    ? $meta['seekable']
156
                                    : false
157
                                   );
158
159
        if (isset($options['writable']) && is_bool($options['writable'])) {
160
            $this->writable = $options['writable'];
161
        } else {
162
            foreach (static::MODES_WRITE as $mode) {
163
                if (strncmp($meta['mode'], $mode, strlen($mode)) === 0) {
164
                    $this->writable = true;
165
                    break;
166
                }
167
            }
168
        }
169
170
        if (isset($options['readable']) && is_bool($options['readable'])) {
171
            $this->readable = $options['readable'];
172
        } else {
173
            foreach (static::MODES_READ as $mode) {
174
                if (strncmp($meta['mode'], $mode, strlen($mode)) === 0) {
175
                    $this->readable = true;
176
                    break;
177
                }
178
            }
179
        }
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185
    public function __toString(): string
186
    {
187
        try {
188
            if ($this->seekable) {
189
                $this->rewind();
190
            }
191
            return $this->getContents();
192
        } catch (Exception $e) {
193
            return '';
194
        }
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function close(): void
201
    {
202
        if ($this->resource !== null && fclose($this->resource)) {
203
            $this->detach();
204
        }
205
    }
206
207
    /**
208
     * {@inheritdoc}
209
     */
210
    public function detach()
211
    {
212
        $resource = $this->resource;
213
        if ($resource !== null) {
214
            $this->resource = null;
215
            $this->size = null;
216
            $this->seekable = false;
217
            $this->writable = false;
218
            $this->readable = false;
219
        }
220
        return $resource;
221
    }
222
223
    /**
224
     * {@inheritdoc}
225
     */
226
    public function getSize(): ?int
227
    {
228
        return $this->size;
229
    }
230
231
    /**
232
     * {@inheritdoc}
233
     */
234
    public function tell(): int
235
    {
236
        if ($this->resource === null) {
237
            throw new RuntimeException('Stream resource is detached');
238
        }
239
        $position = ftell($this->resource);
240
        if ($position === false) {
241
            throw new RuntimeException('Unable to tell the current position of the stream read/write pointer');
242
        }
243
244
        return $position;
245
    }
246
247
    /**
248
     * {@inheritdoc}
249
     */
250
    public function eof(): bool
251
    {
252
        return $this->resource === null || feof($this->resource);
253
    }
254
255
    /**
256
     * {@inheritdoc}
257
     */
258
    public function isSeekable(): bool
259
    {
260
        return $this->seekable;
261
    }
262
263
    /**
264
     * {@inheritdoc}
265
     */
266
    public function seek(int $offset, int $whence = SEEK_SET): void
267
    {
268
        if ($this->resource === null) {
269
            throw new RuntimeException('Stream resource is detached');
270
        }
271
272
        if (!$this->seekable) {
273
            throw new RuntimeException('Stream is not seekable');
274
        }
275
276
        if (fseek($this->resource, $offset, $whence) === -1) {
277
            throw new RuntimeException('Can not seek to a position in the stream');
278
        }
279
    }
280
281
    /**
282
     * {@inheritdoc}
283
     */
284
    public function rewind(): void
285
    {
286
        $this->seek(0);
287
    }
288
289
    /**
290
     * {@inheritdoc}
291
     */
292
    public function isWritable(): bool
293
    {
294
        return $this->writable;
295
    }
296
297
    /**
298
     * {@inheritdoc}
299
     */
300
    public function write(string $string): int
301
    {
302
        if ($this->resource === null) {
303
            throw new RuntimeException('Stream resource is detached');
304
        }
305
306
        if (!$this->writable) {
307
            throw new RuntimeException('Stream is not writable');
308
        }
309
        $bytes = fwrite($this->resource, $string);
310
311
        if ($bytes === false) {
312
            throw new RuntimeException('Unable to write data to the stream');
313
        }
314
315
        $fstat = fstat($this->resource);
316
        if ($fstat === false) {
317
            $this->size = null;
318
        } else {
319
            $this->size = !empty($fstat['size']) ? $fstat['size'] : null;
320
        }
321
322
        return $bytes;
323
    }
324
325
    /**
326
     * {@inheritdoc}
327
     */
328
    public function isReadable(): bool
329
    {
330
        return $this->readable;
331
    }
332
333
    /**
334
     * {@inheritdoc}
335
     */
336
    public function read(int $length): string
337
    {
338
        if ($this->resource === null) {
339
            throw new RuntimeException('Stream resource is detached');
340
        }
341
342
        if (!$this->readable) {
343
            throw new RuntimeException('Stream is not readable');
344
        }
345
346
        $data = fread($this->resource, $length);
347
        if ($data === false) {
348
            throw new RuntimeException('Unable to read data from the stream');
349
        }
350
351
        return $data;
352
    }
353
354
    /**
355
     * {@inheritdoc}
356
     */
357
    public function getContents(): string
358
    {
359
        if ($this->resource === null) {
360
            throw new RuntimeException('Stream resource is detached');
361
        }
362
363
        if (!$this->readable) {
364
            throw new RuntimeException('Stream is not readable');
365
        }
366
367
        $contents = stream_get_contents($this->resource);
368
369
        if ($contents === false) {
370
            throw new RuntimeException('Unable to get contents of the stream');
371
        }
372
373
        return $contents;
374
    }
375
376
    /**
377
     * {@inheritdoc}
378
     */
379
    public function getMetadata(?string $key = null): mixed
380
    {
381
        if ($this->resource === null) {
382
            throw new RuntimeException('Stream resource is detached');
383
        }
384
385
        $meta = stream_get_meta_data($this->resource);
386
        if ($key === null) {
387
            return $meta;
388
        }
389
        return !empty($meta[$key]) ? $meta[$key] : null;
390
    }
391
392
    /**
393
     * Check if the given mode is valid
394
     * @param  string $mode the mode
395
     * @return string
396
     * @throws InvalidArgumentException
397
     */
398
    protected function filterMode(string $mode): string
399
    {
400
        if (!in_array($mode, static::MODES_WRITE) && !in_array($mode, static::MODES_READ)) {
401
            throw new InvalidArgumentException(sprintf('Invalid mode %s', $mode));
402
        }
403
        return $mode;
404
    }
405
}
406