Code

< 40 %
40-60 %
> 60 %
1
<?php
2
3
/*
4
 * This file is part of the Koded package.
5
 *
6
 * (c) Mihail Binev <[email protected]>
7
 *
8
 * Please view the LICENSE distributed with this source code
9
 * for the full copyright and license information.
10
 *
11
 */
12
13
namespace Koded\Http;
14
15
use Koded\Http\Interfaces\HttpStatus;
16
use Psr\Http\Message\StreamInterface;
17
18
19
class Stream implements StreamInterface
20
{
21
    protected const MODES = [
22
        'w+'  => 1,
23
        'r+'  => 1,
24
        'x+'  => 1,
25
        'c+'  => 1,
26
        'a+'  => 1,
27
        'w+b' => 1,
28
        'r+b' => 1,
29
        'x+b' => 1,
30
        'c+b' => 1,
31
        'w+t' => 1,
32
        'r+t' => 1,
33
        'x+t' => 1,
34
        'c+t' => 1
35
    ];
36
37
    /** @var resource The underlying stream resource */
38
    protected $stream;
39
    protected string $mode   = 'w+b';
40
    protected bool $seekable = false;
41
42
    public function __construct($stream)
43
    {
44 210
        if (false === \is_resource($stream) || 'stream' !== \get_resource_type($stream)) {
45
            throw new \RuntimeException(
46 210
                'The provided resource is not a valid stream resource, ' . \gettype($stream) . ' given.',
47 1
                HttpStatus::UNPROCESSABLE_ENTITY
48 1
            );
49 1
        }
50
        $metadata       = \stream_get_meta_data($stream);
51
        $this->mode     = $metadata['mode'] ?? 'w+b';
52
        $this->seekable = $metadata['seekable'] ?? false;
53 209
        $this->stream   = $stream;
54 209
    }
55 209
56 209
    public function __destruct()
57 209
    {
58
        $this->close();
59 142
    }
60
61 142
    public function __toString(): string
62 142
    {
63
        try {
64 30
            $this->seek(0);
65
            return $this->getContents();
66
        } catch (\Throwable) {
67 30
            return '';
68
        }
69 29
    }
70 1
71 1
    public function close(): void
72
    {
73
        if ($this->stream) {
74
            \fclose($this->stream);
75 142
            $this->detach();
76
        }
77 142
    }
78 139
79 139
    public function detach()
80
    {
81 142
        if (empty($this->stream)) {
82
            return null;
83 142
        }
84
        $resource       = $this->stream;
85 142
        $this->mode     = 'w+b';
86 1
        $this->stream   = null;
87
        $this->seekable = false;
88
        return $resource;
89 142
    }
90 142
91 142
    public function getSize(): ?int
92 142
    {
93
        if (empty($this->stream)) {
94 142
            return null;
95
        }
96
        return \fstat($this->stream)['size'] ?? null;
97 36
    }
98
99 36
    public function tell(): int
100 1
    {
101
        if (false === $position = \ftell($this->stream)) {
102
            throw new \RuntimeException('Failed to find the position of the file pointer');
103 35
        }
104
        return $position;
105
    }
106 4
107
    public function eof(): bool
108 4
    {
109 1
        return \feof($this->stream);
110
    }
111
112 3
    public function seek($offset, $whence = SEEK_SET): void
113
    {
114
        if (0 !== @\fseek($this->stream, $offset, $whence)) {
115 6
            throw new \RuntimeException('Failed to seek to file pointer');
116
        }
117 6
    }
118
119
    public function rewind(): void
120 51
    {
121
        if (false === $this->seekable) {
122 51
            throw new \RuntimeException('The stream is not seekable');
123 3
        }
124
        $this->seek(0);
125 48
    }
126
127 16
    public function write($string): int
128
    {
129 16
        if (false === $this->isWritable()) {
130 2
            throw new \RuntimeException('The stream is not writable');
131
        }
132
        if (false === $bytes = \fwrite($this->stream, $string)) {
133 14
            throw new \RuntimeException('Failed to write data to the stream');
134 14
        }
135
        return $bytes;
136 8
    }
137
138 8
    public function read($length): string
139 1
    {
140
        if (false === $this->isReadable()) {
141
            throw new \RuntimeException('The stream is not readable');
142 7
        }
143
        if (empty($length)) {
144
            return '';
145
        }
146 7
        if (false === $data = \fread($this->stream, $length)) {
147
            throw new \RuntimeException('Failed to read the data from stream');
148
        }
149 9
        return $data;
150
    }
151 9
152 2
    public function getContents(): string
153
    {
154
        if (false === $content = \stream_get_contents($this->stream)) {
155 7
            throw new \RuntimeException('Unable to read the stream content');
156 1
        }
157
        return $content;
158
    }
159 6
160
    public function getMetadata($key = null)
161
    {
162
        $metadata = \stream_get_meta_data($this->stream);
163 6
        if (null === $key) {
164
            return $metadata;
165
        }
166 48
        return $metadata[$key] ?? null;
167
    }
168 48
169
    public function isSeekable(): bool
170
    {
171
        return $this->seekable;
172 48
    }
173
174
    public function isReadable(): bool
175 4
    {
176
        return isset((self::MODES + ['r' => 1, 'rb' => 1, 'rt' => 1])[$this->mode]);
177 4
    }
178
179 4
    public function isWritable(): bool
180 1
    {
181
        return isset((self::MODES + ['w' => 1, 'rw' => 1, 'a' => 1, 'wb' => 1])[$this->mode]);
182
    }
183
}
184