Passed
Pull Request — master (#43)
by Anatoly
05:45
created

Stream::write()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 4.0218

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 8
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 16
ccs 8
cts 9
cp 0.8889
crap 4.0218
rs 10
1
<?php declare(strict_types=1);
2
3
/**
4
 * It's free open-source software released under the MIT License.
5
 *
6
 * @author Anatoly Nekhay <[email protected]>
7
 * @copyright Copyright (c) 2018, Anatoly Nekhay
8
 * @license https://github.com/sunrise-php/http-message/blob/master/LICENSE
9
 * @link https://github.com/sunrise-php/http-message
10
 */
11
12
namespace Sunrise\Http\Message;
13
14
use Psr\Http\Message\StreamInterface;
15
use Sunrise\Http\Message\Exception\InvalidArgumentException;
16
use Sunrise\Http\Message\Exception\RuntimeException;
17
use Throwable;
18
19
use function fclose;
20
use function feof;
21
use function fread;
22
use function fseek;
23
use function fstat;
24
use function ftell;
25
use function fwrite;
26
use function is_resource;
27
use function stream_get_contents;
28
use function stream_get_meta_data;
29
use function strpbrk;
30
31
use const SEEK_SET;
32
33
class Stream implements StreamInterface
34
{
35
    /**
36
     * @var resource|null
37
     */
38
    private $resource;
39
40
    private bool $autoClose;
41
42
    /**
43
     * @param mixed $resource
44
     *
45
     * @throws InvalidArgumentException
46
     */
47 163
    public function __construct($resource, bool $autoClose = true)
48
    {
49 163
        if (!is_resource($resource)) {
50 4
            throw new InvalidArgumentException('Unexpected stream resource');
51
        }
52
53 162
        $this->resource = $resource;
54 162
        $this->autoClose = $autoClose;
55
    }
56
57
    /**
58
     * @param mixed $resource
59
     *
60
     * @throws InvalidArgumentException
61
     */
62 3
    public static function create($resource): StreamInterface
63
    {
64 3
        if ($resource instanceof StreamInterface) {
65 1
            return $resource;
66
        }
67
68 2
        return new self($resource);
69
    }
70
71 114
    public function __destruct()
72
    {
73 114
        if ($this->autoClose) {
74 103
            $this->close();
75
        }
76
    }
77
78
    /**
79
     * @inheritDoc
80
     */
81 162
    public function detach()
82
    {
83 162
        $resource = $this->resource;
84 162
        $this->resource = null;
85
86 162
        return $resource;
87
    }
88
89
    /**
90
     * @inheritDoc
91
     */
92 162
    public function close(): void
93
    {
94 162
        $resource = $this->detach();
95 162
        if (!is_resource($resource)) {
96 42
            return;
97
        }
98
99 144
        fclose($resource);
100
    }
101
102
    /**
103
     * @inheritDoc
104
     */
105 4
    public function eof(): bool
106
    {
107 4
        if (!is_resource($this->resource)) {
108 2
            return true;
109
        }
110
111 2
        return feof($this->resource);
112
    }
113
114
    /**
115
     * @inheritDoc
116
     */
117 8
    public function tell(): int
118
    {
119 8
        if (!is_resource($this->resource)) {
120 2
            throw new RuntimeException('Stream has no resource');
121
        }
122
123 6
        $result = ftell($this->resource);
124 6
        if ($result === false) {
125 1
            throw new RuntimeException('Unable to get the stream pointer position');
126
        }
127
128 5
        return $result;
129
    }
130
131
    /**
132
     * @inheritDoc
133
     */
134 43
    public function isSeekable(): bool
135
    {
136 43
        if (!is_resource($this->resource)) {
137 2
            return false;
138
        }
139
140 41
        $metadata = stream_get_meta_data($this->resource);
141
142 41
        return $metadata['seekable'];
143
    }
144
145
    /**
146
     * @inheritDoc
147
     */
148 31
    public function rewind(): void
149
    {
150 31
        $this->seek(0);
151
    }
152
153
    /**
154
     * @inheritDoc
155
     */
156 38
    public function seek($offset, $whence = SEEK_SET): void
157
    {
158 38
        if (!is_resource($this->resource)) {
159 4
            throw new RuntimeException('Stream has no resource');
160
        }
161
162 34
        if (!$this->isSeekable()) {
163 3
            throw new RuntimeException('Stream is not seekable');
164
        }
165
166 31
        $result = fseek($this->resource, $offset, $whence);
167 31
        if ($result !== 0) {
168
            throw new RuntimeException('Unable to move the stream pointer position');
169
        }
170
    }
171
172
    /**
173
     * @inheritDoc
174
     */
175 38
    public function isWritable(): bool
176
    {
177 38
        if (!is_resource($this->resource)) {
178 2
            return false;
179
        }
180
181 36
        $metadata = stream_get_meta_data($this->resource);
182
183 36
        return strpbrk($metadata['mode'], '+acwx') !== false;
184
    }
185
186
    /**
187
     * @inheritDoc
188
     */
189 28
    public function write($string): int
190
    {
191 28
        if (!is_resource($this->resource)) {
192 2
            throw new RuntimeException('Stream has no resource');
193
        }
194
195 26
        if (!$this->isWritable()) {
196 1
            throw new RuntimeException('Stream is not writable');
197
        }
198
199 25
        $result = fwrite($this->resource, $string);
200 25
        if ($result === false) {
201
            throw new RuntimeException('Unable to write to the stream');
202
        }
203
204 25
        return $result;
205
    }
206
207
    /**
208
     * @inheritDoc
209
     */
210 39
    public function isReadable(): bool
211
    {
212 39
        if (!is_resource($this->resource)) {
213 4
            return false;
214
        }
215
216 35
        $metadata = stream_get_meta_data($this->resource);
217
218 35
        return strpbrk($metadata['mode'], '+r') !== false;
219
    }
220
221
    /**
222
     * @inheritDoc
223
     *
224
     * @psalm-param int $length
225
     * @phpstan-param int<1, max> $length
226
     */
227 8
    public function read($length): string
228
    {
229 8
        if (!is_resource($this->resource)) {
230 2
            throw new RuntimeException('Stream has no resource');
231
        }
232
233 6
        if (!$this->isReadable()) {
234 1
            throw new RuntimeException('Stream is not readable');
235
        }
236
237 5
        $result = fread($this->resource, $length);
238 5
        if ($result === false) {
239
            throw new RuntimeException('Unable to read from the stream');
240
        }
241
242 5
        return $result;
243
    }
244
245
    /**
246
     * @inheritDoc
247
     */
248 21
    public function getContents(): string
249
    {
250 21
        if (!is_resource($this->resource)) {
251 3
            throw new RuntimeException('Stream has no resource');
252
        }
253
254 18
        if (!$this->isReadable()) {
255 1
            throw new RuntimeException('Stream is not readable');
256
        }
257
258 17
        $result = stream_get_contents($this->resource);
259 17
        if ($result === false) {
260
            throw new RuntimeException('Unable to read the remainder of the stream');
261
        }
262
263 17
        return $result;
264
    }
265
266
    /**
267
     * @inheritDoc
268
     */
269 26
    public function getMetadata($key = null)
270
    {
271 26
        if (!is_resource($this->resource)) {
272 2
            return null;
273
        }
274
275 24
        $metadata = stream_get_meta_data($this->resource);
276 24
        if ($key === null) {
277 1
            return $metadata;
278
        }
279
280 23
        return $metadata[$key] ?? null;
281
    }
282
283
    /**
284
     * @inheritDoc
285
     */
286 7
    public function getSize(): ?int
287
    {
288 7
        if (!is_resource($this->resource)) {
289 2
            return null;
290
        }
291
292
        /** @var array{size: int}|false */
293 5
        $stats = fstat($this->resource);
294 5
        if ($stats === false) {
295
            return null;
296
        }
297
298 5
        return $stats['size'];
299
    }
300
301
    /**
302
     * @inheritDoc
303
     */
304 17
    public function __toString(): string
305
    {
306 17
        if (!$this->isReadable()) {
307 3
            return '';
308
        }
309
310
        try {
311 14
            if ($this->isSeekable()) {
312 14
                $this->rewind();
313
            }
314
315 14
            return $this->getContents();
316
        } catch (Throwable $e) {
317
            return '';
318
        }
319
    }
320
}
321