Passed
Pull Request — master (#41)
by kenny
06:25
created

Stream::write()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 9
nc 4
nop 1
dl 0
loc 18
rs 9.9666
c 0
b 0
f 0
1
<?php
2
3
namespace One\Http;
4
5
/**
6
 *
7
 */
8
class Stream implements \Psr\Http\Message\StreamInterface
9
{
10
    private $stream;
11
    private $size;
12
    private $seekable;
13
    private $readable;
14
    private $writable;
15
    private $uri;
16
    private $customMetadata;
17
18
    /** @var array Hash of readable and writable stream types */
19
    private static $readWriteHash = [
20
        'read'  => [
21
            'r'   => true, 'w+'  => true, 'r+'  => true, 'x+'  => true, 'c+' => true,
22
            'rb'  => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
23
            'c+b' => true, 'rt'  => true, 'w+t' => true, 'r+t' => true,
24
            'x+t' => true, 'c+t' => true, 'a+'  => true,
25
        ],
26
        'write' => [
27
            'w'   => true, 'w+'  => true, 'rw'  => true, 'r+'  => true, 'x+' => true,
28
            'c+'  => true, 'wb'  => true, 'w+b' => true, 'r+b' => true,
29
            'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
30
            'x+t' => true, 'c+t' => true, 'a'   => true, 'a+'  => true,
31
        ],
32
    ];
33
34
    /**
35
     * This constructor accepts an associative array of options.
36
     *
37
     * - size: (int) If a read stream would otherwise have an indeterminate
38
     *   size, but the size is known due to foreknowledge, then you can
39
     *   provide that size, in bytes.
40
     * - metadata: (array) Any additional metadata to return when the metadata
41
     *   of the stream is accessed.
42
     *
43
     * @param resource $stream  Stream resource to wrap.
44
     * @param array    $options Associative array of options.
45
     *
46
     * @throws \InvalidArgumentException if the stream is not a stream resource
47
     */
48
    public function __construct($stream, $options = [])
49
    {
50
        if (!is_resource($stream)) {
51
            throw new \InvalidArgumentException('Stream must be a resource');
52
        }
53
54
        if (isset($options['size'])) {
55
            $this->size = $options['size'];
56
        }
57
58
        $this->customMetadata = isset($options['metadata'])
59
        ? $options['metadata']
60
        : [];
61
62
        $this->stream   = $stream;
63
        $meta           = stream_get_meta_data($this->stream);
64
        $this->seekable = $meta['seekable'];
65
        $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]);
66
        $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]);
67
        $this->uri      = $this->getMetadata('uri');
68
    }
69
70
    /**
71
     * Closes the stream when the destructed
72
     */
73
    public function __destruct()
74
    {
75
        $this->close();
76
    }
77
78
    public function __toString()
79
    {
80
        try {
81
            $this->seek(0);
82
            return (string) stream_get_contents($this->stream);
83
        } catch (\Exception $e) {
84
            return '';
85
        }
86
    }
87
88
    public function getContents()
89
    {
90
        if (!isset($this->stream)) {
91
            throw new \RuntimeException('Stream is detached');
92
        }
93
94
        $contents = stream_get_contents($this->stream);
95
96
        if ($contents === false) {
97
            throw new \RuntimeException('Unable to read stream contents');
98
        }
99
100
        return $contents;
101
    }
102
103
    public function close()
104
    {
105
        if (isset($this->stream)) {
106
            if (is_resource($this->stream)) {
107
                fclose($this->stream);
108
            }
109
            $this->detach();
110
        }
111
    }
112
113
    public function detach()
114
    {
115
        if (!isset($this->stream)) {
116
            return null;
117
        }
118
119
        $result = $this->stream;
120
        unset($this->stream);
121
        $this->size     = $this->uri     = null;
122
        $this->readable = $this->writable = $this->seekable = false;
123
124
        return $result;
125
    }
126
127
    public function getSize()
128
    {
129
        if ($this->size !== null) {
130
            return $this->size;
131
        }
132
133
        if (!isset($this->stream)) {
134
            return null;
135
        }
136
137
        // Clear the stat cache if the stream has a URI
138
        if ($this->uri) {
139
            clearstatcache(true, $this->uri);
140
        }
141
142
        $stats = fstat($this->stream);
143
        if (isset($stats['size'])) {
144
            $this->size = $stats['size'];
145
            return $this->size;
146
        }
147
148
        return null;
149
    }
150
151
    public function isReadable()
152
    {
153
        return $this->readable;
154
    }
155
156
    public function isWritable()
157
    {
158
        return $this->writable;
159
    }
160
161
    public function isSeekable()
162
    {
163
        return $this->seekable;
164
    }
165
166
    public function eof()
167
    {
168
        if (!isset($this->stream)) {
169
            throw new \RuntimeException('Stream is detached');
170
        }
171
172
        return feof($this->stream);
173
    }
174
175
    public function tell()
176
    {
177
        if (!isset($this->stream)) {
178
            throw new \RuntimeException('Stream is detached');
179
        }
180
181
        $result = ftell($this->stream);
182
183
        if ($result === false) {
184
            throw new \RuntimeException('Unable to determine stream position');
185
        }
186
187
        return $result;
188
    }
189
190
    public function rewind()
191
    {
192
        $this->seek(0);
193
    }
194
195
    public function seek($offset, $whence = SEEK_SET)
196
    {
197
        if (!isset($this->stream)) {
198
            throw new \RuntimeException('Stream is detached');
199
        }
200
        if (!$this->seekable) {
201
            throw new \RuntimeException('Stream is not seekable');
202
        }
203
        if (fseek($this->stream, $offset, $whence) === -1) {
204
            throw new \RuntimeException('Unable to seek to stream position '
205
                . $offset . ' with whence ' . var_export($whence, true));
206
        }
207
    }
208
209
    public function read($length)
210
    {
211
        if (!isset($this->stream)) {
212
            throw new \RuntimeException('Stream is detached');
213
        }
214
        if (!$this->readable) {
215
            throw new \RuntimeException('Cannot read from non-readable stream');
216
        }
217
        if ($length < 0) {
218
            throw new \RuntimeException('Length parameter cannot be negative');
219
        }
220
221
        if (0 === $length) {
222
            return '';
223
        }
224
225
        $string = fread($this->stream, $length);
226
        if (false === $string) {
227
            throw new \RuntimeException('Unable to read from stream');
228
        }
229
230
        return $string;
231
    }
232
233
    public function write($string)
234
    {
235
        if (!isset($this->stream)) {
236
            throw new \RuntimeException('Stream is detached');
237
        }
238
        if (!$this->writable) {
239
            throw new \RuntimeException('Cannot write to a non-writable stream');
240
        }
241
242
        // We can't know the size after writing anything
243
        $this->size = null;
244
        $result     = fwrite($this->stream, $string);
245
246
        if ($result === false) {
247
            throw new \RuntimeException('Unable to write to stream');
248
        }
249
250
        return $result;
251
    }
252
253
    public function getMetadata($key = null)
254
    {
255
        if (!isset($this->stream)) {
256
            return $key ? null : [];
257
        } elseif (!$key) {
258
            return $this->customMetadata + stream_get_meta_data($this->stream);
259
        } elseif (isset($this->customMetadata[$key])) {
260
            return $this->customMetadata[$key];
261
        }
262
263
        $meta = stream_get_meta_data($this->stream);
264
265
        return isset($meta[$key]) ? $meta[$key] : null;
266
    }
267
}
268