• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

voku / httpful / 5623107679

pending completion
5623107679

push

github

voku
[+]: fix test for the new release

1596 of 2486 relevant lines covered (64.2%)

81.28 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.43
/src/Httpful/Stream.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Httpful;
6

7
use Psr\Http\Message\StreamInterface;
8
use voku\helper\UTF8;
9

10
class Stream implements StreamInterface
11
{
12
    /**
13
     * Resource modes.
14
     *
15
     * @var string
16
     *
17
     * @see http://php.net/manual/function.fopen.php
18
     * @see http://php.net/manual/en/function.gzopen.php
19
     */
20
    const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/';
21

22
    /**
23
     * @var array<string, array<string, bool>> Hash of readable and writable stream types
24
     */
25
    const READ_WRITE_HASH = [
26
        'read' => [
27
            'r'   => true,
28
            'w+'  => true,
29
            'r+'  => true,
30
            'x+'  => true,
31
            'c+'  => true,
32
            'rb'  => true,
33
            'w+b' => true,
34
            'r+b' => true,
35
            'x+b' => true,
36
            'c+b' => true,
37
            'rt'  => true,
38
            'w+t' => true,
39
            'r+t' => true,
40
            'x+t' => true,
41
            'c+t' => true,
42
            'a+'  => true,
43
        ],
44
        'write' => [
45
            'w'   => true,
46
            'w+'  => true,
47
            'rw'  => true,
48
            'r+'  => true,
49
            'x+'  => true,
50
            'c+'  => true,
51
            'wb'  => true,
52
            'w+b' => true,
53
            'r+b' => true,
54
            'x+b' => true,
55
            'c+b' => true,
56
            'w+t' => true,
57
            'r+t' => true,
58
            'x+t' => true,
59
            'c+t' => true,
60
            'a'   => true,
61
            'a+'  => true,
62
        ],
63
    ];
64

65
    /**
66
     * @var string
67
     */
68
    const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/';
69

70
    /**
71
     * @var resource|null
72
     */
73
    private $stream;
74

75
    /**
76
     * @var int|null
77
     */
78
    private $size;
79

80
    /**
81
     * @var bool
82
     */
83
    private $seekable;
84

85
    /**
86
     * @var bool
87
     */
88
    private $readable;
89

90
    /**
91
     * @var bool
92
     */
93
    private $writable;
94

95
    /**
96
     * @var string|null
97
     */
98
    private $uri;
99

100
    /**
101
     * @var array
102
     */
103
    private $customMetadata;
104

105
    /**
106
     * @var bool
107
     */
108
    private $serialized;
109

110
    /**
111
     * This constructor accepts an associative array of options.
112
     *
113
     * - size: (int) If a read stream would otherwise have an indeterminate
114
     *   size, but the size is known due to foreknowledge, then you can
115
     *   provide that size, in bytes.
116
     * - metadata: (array) Any additional metadata to return when the metadata
117
     *   of the stream is accessed.
118
     *
119
     * @param resource            $stream  stream resource to wrap
120
     * @param array<string,mixed> $options associative array of options
121
     *
122
     * @throws \InvalidArgumentException if the stream is not a stream resource
123
     */
124
    public function __construct($stream, $options = [])
125
    {
126
        if (!\is_resource($stream)) {
424✔
127
            throw new \InvalidArgumentException('Stream must be a resource');
×
128
        }
129

130
        if (isset($options['size'])) {
424✔
131
            $this->size = (int) $options['size'];
×
132
        }
133

134
        $this->customMetadata = $options['metadata'] ?? [];
424✔
135

136
        $this->serialized = $options['serialized'] ?? false;
424✔
137

138
        $this->stream = $stream;
424✔
139
        $meta = \stream_get_meta_data($this->stream);
424✔
140
        $this->seekable = (bool) $meta['seekable'];
424✔
141
        $this->readable = (bool) \preg_match(self::READABLE_MODES, $meta['mode']);
424✔
142
        $this->writable = (bool) \preg_match(self::WRITABLE_MODES, $meta['mode']);
424✔
143
        $this->uri = $this->getMetadata('uri');
424✔
144
    }
145

146
    /**
147
     * Closes the stream when the destructed
148
     */
149
    public function __destruct()
150
    {
151
        $this->close();
424✔
152
    }
153

154
    /**
155
     * @return string
156
     */
157
    public function __toString(): string
158
    {
159
        try {
160
            $this->seek(0);
332✔
161

162
            if ($this->stream === null) {
328✔
163
                return '';
×
164
            }
165

166
            return (string) \stream_get_contents($this->stream);
328✔
167
        } catch (\Exception $e) {
4✔
168
            return '';
4✔
169
        }
170
    }
171

172
    public function close(): void
173
    {
174
        if (isset($this->stream)) {
424✔
175
            if (\is_resource($this->stream)) {
416✔
176
                \fclose($this->stream);
416✔
177
            }
178

179
            /** @noinspection UnusedFunctionResultInspection */
180
            $this->detach();
416✔
181
        }
182
    }
183

184
    /**
185
     * @return resource|null
186
     */
187
    public function detach()
188
    {
189
        if (!isset($this->stream)) {
424✔
190
            return null;
4✔
191
        }
192

193
        $result = $this->stream;
424✔
194
        $this->stream = null;
424✔
195
        $this->size = null;
424✔
196
        $this->uri = null;
424✔
197
        $this->readable = false;
424✔
198
        $this->writable = false;
424✔
199
        $this->seekable = false;
424✔
200

201
        return $result;
424✔
202
    }
203

204
    /**
205
     * @return bool
206
     */
207
    public function eof(): bool
208
    {
209
        if (!isset($this->stream)) {
24✔
210
            throw new \RuntimeException('Stream is detached');
4✔
211
        }
212

213
        return \feof($this->stream);
20✔
214
    }
215

216
    /**
217
     * @return mixed
218
     */
219
    public function getContentsUnserialized()
220
    {
221
        $contents = $this->getContents();
4✔
222

223
        if ($this->serialized) {
4✔
224
            /** @noinspection UnserializeExploitsInspection */
225
            $contents = \unserialize($contents, []);
4✔
226
        }
227

228
        return $contents;
4✔
229
    }
230

231
    public function getContents(): string
232
    {
233
        if (!isset($this->stream)) {
52✔
234
            throw new \RuntimeException('Stream is detached');
4✔
235
        }
236

237
        $contents = \stream_get_contents($this->stream);
48✔
238
        if ($contents === false) {
48✔
239
            throw new \RuntimeException('Unable to read stream contents');
×
240
        }
241

242
        return $contents;
48✔
243
    }
244

245
    /**
246
     * @param string|null $key
247
     *
248
     * @return array|mixed|null
249
     */
250
    public function getMetadata($key = null)
251
    {
252
        if (!isset($this->stream)) {
424✔
253
            return $key ? null : [];
4✔
254
        }
255

256
        if (!$key) {
424✔
257
            /** @noinspection AdditionOperationOnArraysInspection */
258
            return $this->customMetadata + \stream_get_meta_data($this->stream);
4✔
259
        }
260

261
        if (isset($this->customMetadata[$key])) {
424✔
262
            return $this->customMetadata[$key];
×
263
        }
264

265
        $meta = \stream_get_meta_data($this->stream);
424✔
266

267
        return $meta[$key] ?? null;
424✔
268
    }
269

270
    /**
271
     * @return int|null
272
     */
273
    public function getSize(): ?int
274
    {
275
        if ($this->size !== null) {
80✔
276
            return $this->size;
12✔
277
        }
278

279
        if (!isset($this->stream)) {
80✔
280
            return null;
8✔
281
        }
282

283
        // Clear the stat cache if the stream has a URI
284
        if ($this->uri) {
72✔
285
            \clearstatcache(true, $this->uri);
72✔
286
        }
287

288
        $stats = \fstat($this->stream);
72✔
289
        if ($stats !== false) {
72✔
290
            $this->size = $stats['size'];
72✔
291

292
            return $this->size;
72✔
293
        }
294

295
        return null;
×
296
    }
297

298
    /**
299
     * @return bool
300
     */
301
    public function isReadable(): bool
302
    {
303
        return $this->readable;
12✔
304
    }
305

306
    /**
307
     * @return bool
308
     */
309
    public function isSeekable(): bool
310
    {
311
        return $this->seekable;
24✔
312
    }
313

314
    /**
315
     * @return bool
316
     */
317
    public function isWritable(): bool
318
    {
319
        return $this->writable;
12✔
320
    }
321

322
    /**
323
     * @param int $length
324
     *
325
     * @return string
326
     */
327
    public function read($length): string
328
    {
329
        if (!isset($this->stream)) {
20✔
330
            throw new \RuntimeException('Stream is detached');
4✔
331
        }
332

333
        if (!$this->readable) {
16✔
334
            throw new \RuntimeException('Cannot read from non-readable stream');
×
335
        }
336

337
        if ($length < 0) {
16✔
338
            throw new \RuntimeException('Length parameter cannot be negative');
×
339
        }
340

341
        if ($length === 0) {
16✔
342
            return '';
×
343
        }
344

345
        $string = \fread($this->stream, $length);
16✔
346
        if ($string === false) {
16✔
347
            throw new \RuntimeException('Unable to read from stream');
×
348
        }
349

350
        return $string;
16✔
351
    }
352

353
    /**
354
     * @return void
355
     */
356
    public function rewind(): void
357
    {
358
        $this->seek(0);
12✔
359
    }
360

361
    /**
362
     * @param int $offset
363
     * @param int $whence
364
     *
365
     * @return void
366
     */
367
    public function seek($offset, $whence = \SEEK_SET): void
368
    {
369
        $whence = (int) $whence;
348✔
370

371
        if (!isset($this->stream)) {
348✔
372
            throw new \RuntimeException('Stream is detached');
4✔
373
        }
374
        if (!$this->seekable) {
344✔
375
            throw new \RuntimeException('Stream is not seekable');
×
376
        }
377
        if (\fseek($this->stream, $offset, $whence) === -1) {
344✔
378
            throw new \RuntimeException(
×
379
                'Unable to seek to stream position '
×
380
                . $offset . ' with whence ' . \var_export($whence, true)
×
381
            );
×
382
        }
383
    }
384

385
    /**
386
     * @return int
387
     */
388
    public function tell(): int
389
    {
390
        if (!isset($this->stream)) {
8✔
391
            throw new \RuntimeException('Stream is detached');
4✔
392
        }
393

394
        $result = \ftell($this->stream);
4✔
395
        if ($result === false) {
4✔
396
            throw new \RuntimeException('Unable to determine stream position');
×
397
        }
398

399
        return $result;
4✔
400
    }
401

402
    /**
403
     * @param string $string
404
     *
405
     * @return int
406
     */
407
    public function write($string): int
408
    {
409
        if (!isset($this->stream)) {
24✔
410
            throw new \RuntimeException('Stream is detached');
4✔
411
        }
412
        if (!$this->writable) {
24✔
413
            throw new \RuntimeException('Cannot write to a non-writable stream');
×
414
        }
415

416
        // We can't know the size after writing anything
417
        $this->size = null;
24✔
418
        $result = \fwrite($this->stream, $string);
24✔
419

420
        if ($result === false) {
24✔
421
            throw new \RuntimeException('Unable to write to stream');
×
422
        }
423

424
        return $result;
24✔
425
    }
426

427
    /**
428
     * Creates a new PSR-7 stream.
429
     *
430
     * @param mixed $body
431
     *
432
     * @return StreamInterface|null
433
     */
434
    public static function create($body = '')
435
    {
436
        if ($body instanceof StreamInterface) {
392✔
437
            return $body;
4✔
438
        }
439

440
        if ($body === null) {
392✔
441
            $body = '';
76✔
442
            $serialized = false;
76✔
443
        } elseif (\is_numeric($body)) {
392✔
444
            $body = (string) $body;
40✔
445
            $serialized = UTF8::is_serialized($body);
40✔
446
        } elseif (
447
            \is_array($body)
356✔
448
            ||
449
            $body instanceof \Serializable
356✔
450
        ) {
451
            $body = \serialize($body);
72✔
452
            $serialized = true;
72✔
453
        } else {
454
            $serialized = false;
356✔
455
        }
456

457
        if (\is_string($body)) {
392✔
458
            $resource = \fopen('php://temp', 'rwb+');
352✔
459
            if ($resource !== false) {
352✔
460
                \fwrite($resource, $body);
352✔
461
                $body = $resource;
352✔
462
            }
463
        }
464

465
        if (\is_resource($body)) {
392✔
466
            $new = new static($body);
392✔
467
            if ($new->stream === null) {
392✔
468
                return null;
×
469
            }
470

471
            $meta = \stream_get_meta_data($new->stream);
392✔
472
            $new->serialized = $serialized;
392✔
473
            $new->seekable = $meta['seekable'];
392✔
474
            $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
392✔
475
            $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
392✔
476
            $new->uri = $new->getMetadata('uri');
392✔
477

478
            return $new;
392✔
479
        }
480

481
        return null;
28✔
482
    }
483

484
    /**
485
     * @param mixed $body
486
     *
487
     * @return StreamInterface
488
     */
489
    public static function createNotNull($body = ''): StreamInterface
490
    {
491
        $stream = static::create($body);
352✔
492
        if ($stream === null) {
352✔
493
            $stream = static::create();
28✔
494
        }
495

496
        \assert($stream instanceof self);
497

498
        return $stream;
352✔
499
    }
500
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc