Passed
Pull Request — master (#56)
by Charis
01:50
created

Stream::__construct()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 20
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 12
nc 5
nop 2
dl 0
loc 20
rs 9.8666
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 bool|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
     * @inheritDoc
79
     */
80
    public function __toString()
81
    {
82
        try {
83
            $this->seek(0);
84
            return (string) stream_get_contents($this->stream);
85
        } catch (\Exception $e) {
86
            return '';
87
        }
88
    }
89
    /**
90
     * @inheritDoc
91
     */
92
    public function getContents()
93
    {
94
        if (!isset($this->stream)) {
95
            throw new \RuntimeException('Stream is detached');
96
        }
97
98
        $contents = stream_get_contents($this->stream);
99
100
        if ($contents === false) {
101
            throw new \RuntimeException('Unable to read stream contents');
102
        }
103
104
        return $contents;
105
    }
106
    /**
107
     * @inheritDoc
108
     */
109
    public function close()
110
    {
111
        if (isset($this->stream)) {
112
            if (is_resource($this->stream)) {
113
                fclose($this->stream);
114
            }
115
            $this->detach();
116
        }
117
    }
118
    /**
119
     * @inheritDoc
120
     */
121
    public function detach()
122
    {
123
        if (!isset($this->stream)) {
124
            return null;
125
        }
126
127
        $result = $this->stream;
128
        unset($this->stream);
129
        $this->size     = $this->uri = null;
130
        $this->readable = $this->writable = $this->seekable = false;
131
132
        return $result;
133
    }
134
    /**
135
     * @inheritDoc
136
     */
137
    public function getSize()
138
    {
139
        if ($this->size !== null) {
140
            return $this->size;
141
        }
142
143
        if (!isset($this->stream)) {
144
            return null;
145
        }
146
147
        // Clear the stat cache if the stream has a URI
148
        if ($this->uri) {
149
            clearstatcache(true, $this->uri);
150
        }
151
152
        $stats = fstat($this->stream);
153
        if (isset($stats['size'])) {
154
            $this->size = $stats['size'];
155
            return $this->size;
156
        }
157
158
        return null;
159
    }
160
    /**
161
     * @inheritDoc
162
     */
163
    public function isReadable()
164
    {
165
        return $this->readable;
166
    }
167
    /**
168
     * @inheritDoc
169
     */
170
    public function isWritable()
171
    {
172
        return $this->writable;
173
    }
174
    /**
175
     * @inheritDoc
176
     */
177
    public function isSeekable()
178
    {
179
        return $this->seekable;
180
    }
181
    /**
182
     * @inheritDoc
183
     */
184
    public function eof()
185
    {
186
        if (!isset($this->stream)) {
187
            throw new \RuntimeException('Stream is detached');
188
        }
189
190
        return feof($this->stream);
191
    }
192
    /**
193
     * @inheritDoc
194
     */
195
    public function tell()
196
    {
197
        if (!isset($this->stream)) {
198
            throw new \RuntimeException('Stream is detached');
199
        }
200
201
        $result = ftell($this->stream);
202
203
        if ($result === false) {
204
            throw new \RuntimeException('Unable to determine stream position');
205
        }
206
207
        return $result;
208
    }
209
    /**
210
     * @inheritDoc
211
     */
212
    public function rewind()
213
    {
214
        $this->seek(0);
215
    }
216
    /**
217
     * @inheritDoc
218
     */
219
    public function seek($offset, $whence = SEEK_SET)
220
    {
221
        if (!isset($this->stream)) {
222
            throw new \RuntimeException('Stream is detached');
223
        }
224
        if (!$this->seekable) {
225
            throw new \RuntimeException('Stream is not seekable');
226
        }
227
        if (fseek($this->stream, $offset, $whence) === -1) {
228
            throw new \RuntimeException('Unable to seek to stream position '
229
                . $offset . ' with whence ' . var_export($whence, true));
230
        }
231
    }
232
    /**
233
     * @inheritDoc
234
     */
235
    public function read($length)
236
    {
237
        if (!isset($this->stream)) {
238
            throw new \RuntimeException('Stream is detached');
239
        }
240
        if (!$this->readable) {
241
            throw new \RuntimeException('Cannot read from non-readable stream');
242
        }
243
        if ($length < 0) {
244
            throw new \RuntimeException('Length parameter cannot be negative');
245
        }
246
247
        if (0 === $length) {
248
            return '';
249
        }
250
251
        $string = fread($this->stream, $length);
252
        if (false === $string) {
253
            throw new \RuntimeException('Unable to read from stream');
254
        }
255
256
        return $string;
257
    }
258
    /**
259
     * @inheritDoc
260
     */
261
    public function write($string)
262
    {
263
        if (!isset($this->stream)) {
264
            throw new \RuntimeException('Stream is detached');
265
        }
266
        if (!$this->writable) {
267
            throw new \RuntimeException('Cannot write to a non-writable stream');
268
        }
269
270
        // We can't know the size after writing anything
271
        $this->size = null;
272
        $result     = fwrite($this->stream, $string);
273
274
        if ($result === false) {
275
            throw new \RuntimeException('Unable to write to stream');
276
        }
277
278
        return $result;
279
    }
280
    /**
281
     * @inheritDoc
282
     */
283
    public function getMetadata($key = null)
284
    {
285
        if (!isset($this->stream)) {
286
            return $key ? null : [];
287
        } elseif (!$key) {
288
            return $this->customMetadata + stream_get_meta_data($this->stream);
289
        } elseif (isset($this->customMetadata[$key])) {
290
            return $this->customMetadata[$key];
291
        }
292
293
        $meta = stream_get_meta_data($this->stream);
294
295
        return isset($meta[$key]) ? $meta[$key] : null;
296
    }
297
}
298