Completed
Push — master ( 3206cc...899138 )
by David
24:10 queued 23:01
created

BufferedStream::isSeekable()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Http\Message\Stream;
4
5
use Psr\Http\Message\StreamInterface;
6
7
/**
8
 * Decorator to make any stream seekable.
9
 *
10
 * Internally it buffers an existing StreamInterface into a php://temp resource (or memory). By default it will use
11
 * 2 megabytes of memory before writing to a temporary disk file.
12
 *
13
 * Due to this, very large stream can suffer performance issue (i/o slowdown).
14
 */
15
class BufferedStream implements StreamInterface
16
{
17
    /** @var resource The buffered resource used to seek previous data */
18
    private $resource;
19
20
    /** @var int size of the stream if available */
21
    private $size;
22
23
    /** @var StreamInterface The underlying stream decorated by this class */
24
    private $stream;
25
26
    /** @var int How many bytes were written */
27
    private $written = 0;
28
29
    /**
30
     * @param StreamInterface $stream        Decorated stream
31
     * @param bool            $useFileBuffer Whether to use a file buffer (write to a file, if data exceed a certain size)
32
     *                                       by default, set this to false to only use memory
33
     * @param int             $memoryBuffer  In conjunction with using file buffer, limit (in bytes) from which it begins to buffer
34
     *                                       the data in a file
35
     */
36 13
    public function __construct(StreamInterface $stream, $useFileBuffer = true, $memoryBuffer = 2097152)
37
    {
38 13
        $this->stream = $stream;
39 13
        $this->size = $stream->getSize();
40
41 13
        if ($useFileBuffer) {
42 13
            $this->resource = fopen('php://temp/maxmemory:'.$memoryBuffer, 'rw+');
43
        } else {
44
            $this->resource = fopen('php://memory', 'rw+');
45
        }
46
47 13
        if (false === $this->resource) {
48
            throw new \RuntimeException('Cannot create a resource over temp or memory implementation');
49
        }
50 13
    }
51
52
    /**
53
     * {@inheritdoc}
54
     */
55 1
    public function __toString()
56
    {
57
        try {
58 1
            $this->rewind();
59
60 1
            return $this->getContents();
61
        } catch (\Throwable $throwable) {
62
            return '';
63
        } catch (\Exception $exception) { // Layer to be BC with PHP 5, remove this when we only support PHP 7+
64
            return '';
65
        }
66
    }
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    public function close()
72
    {
73
        if (null === $this->resource) {
74
            throw new \RuntimeException('Cannot close on a detached stream');
75
        }
76
77
        $this->stream->close();
78
        fclose($this->resource);
79
    }
80
81
    /**
82
     * {@inheritdoc}
83
     */
84 1
    public function detach()
85
    {
86 1
        if (null === $this->resource) {
87 1
            return;
88
        }
89
90
        // Force reading the remaining data of the stream
91 1
        $this->getContents();
92
93 1
        $resource = $this->resource;
94 1
        $this->stream->close();
95 1
        $this->stream = null;
96 1
        $this->resource = null;
97
98 1
        return $resource;
99
    }
100
101
    /**
102
     * {@inheritdoc}
103
     */
104 1
    public function getSize()
105
    {
106 1
        if (null === $this->resource) {
107
            return;
108
        }
109
110 1
        if (null === $this->size && $this->stream->eof()) {
111 1
            return $this->written;
112
        }
113
114 1
        return $this->size;
115
    }
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 3
    public function tell()
121
    {
122 3
        if (null === $this->resource) {
123
            throw new \RuntimeException('Cannot tell on a detached stream');
124
        }
125
126 3
        return ftell($this->resource);
127
    }
128
129
    /**
130
     * {@inheritdoc}
131
     */
132 8
    public function eof()
133
    {
134 8
        if (null === $this->resource) {
135
            throw new \RuntimeException('Cannot call eof on a detached stream');
136
        }
137
138
        // We are at the end only when both our resource and underlying stream are at eof
139 8
        return $this->stream->eof() && (ftell($this->resource) === $this->written);
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 1
    public function isSeekable()
146
    {
147 1
        return null !== $this->resource;
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 2
    public function seek($offset, $whence = SEEK_SET)
154
    {
155 2
        if (null === $this->resource) {
156
            throw new \RuntimeException('Cannot seek on a detached stream');
157
        }
158
159 2
        fseek($this->resource, $offset, $whence);
160 2
    }
161
162
    /**
163
     * {@inheritdoc}
164
     */
165 3
    public function rewind()
166
    {
167 3
        if (null === $this->resource) {
168
            throw new \RuntimeException('Cannot rewind on a detached stream');
169
        }
170
171 3
        rewind($this->resource);
172 3
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 1
    public function isWritable()
178
    {
179 1
        return false;
180
    }
181
182
    /**
183
     * {@inheritdoc}
184
     */
185 1
    public function write($string)
186
    {
187 1
        throw new \RuntimeException('Cannot write on this stream');
188
    }
189
190
    /**
191
     * {@inheritdoc}
192
     */
193 1
    public function isReadable()
194
    {
195 1
        return null !== $this->resource;
196
    }
197
198
    /**
199
     * {@inheritdoc}
200
     */
201 8
    public function read($length)
202
    {
203 8
        if (null === $this->resource) {
204
            throw new \RuntimeException('Cannot read on a detached stream');
205
        }
206
207 8
        $read = '';
208
209
        // First read from the resource
210 8
        if (ftell($this->resource) !== $this->written) {
211 1
            $read = fread($this->resource, $length);
212
        }
213
214 8
        $bytesRead = strlen($read);
215
216 8
        if ($bytesRead < $length) {
217 8
            $streamRead = $this->stream->read($length - $bytesRead);
218
219
            // Write on the underlying stream what we read
220 8
            $this->written += fwrite($this->resource, $streamRead);
221 8
            $read .= $streamRead;
222
        }
223
224 8
        return $read;
225
    }
226
227
    /**
228
     * {@inheritdoc}
229
     */
230 8
    public function getContents()
231
    {
232 8
        if (null === $this->resource) {
233
            throw new \RuntimeException('Cannot read on a detached stream');
234
        }
235
236 8
        $read = '';
237
238 8
        while (!$this->eof()) {
239 7
            $read .= $this->read(8192);
240
        }
241
242 8
        return $read;
243
    }
244
245
    /**
246
     * {@inheritdoc}
247
     */
248 1
    public function getMetadata($key = null)
249
    {
250 1
        if (null === $this->resource) {
251
            if (null === $key) {
252
                return [];
253
            }
254
255
            return;
256
        }
257
258 1
        $metadata = stream_get_meta_data($this->resource);
259
260 1
        if (null === $key) {
261 1
            return $metadata;
262
        }
263
264 1
        if (!array_key_exists($key, $metadata)) {
265 1
            return;
266
        }
267
268 1
        return $metadata[$key];
269
    }
270
}
271