AppendStream::isWritable()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
3
namespace Thruster\Component\HttpMessage;
4
5
use Psr\Http\Message\StreamInterface;
6
7
/**
8
 * Class AppendStream
9
 *
10
 * @package Thruster\Component\HttpMessage
11
 * @author  Aurimas Niekis <[email protected]>
12
 */
13
class AppendStream implements StreamInterface
14
{
15
    /**
16
     * @var StreamInterface[] Streams being decorated
17
     */
18
    private $streams;
19
20
    /**
21
     * @var bool
22
     */
23
    private $seekable;
24
25
    /**
26
     * @var int
27
     */
28
    private $current;
29
30
    /**
31
     * @var int
32
     */
33
    private $pos;
34
35
    /**
36
     * @var bool
37
     */
38
    private $detached;
39
40
    /**
41
     * @param StreamInterface[] $streams Streams to decorate. Each stream must
42
     *                                   be readable.
43
     */
44 23
    public function __construct(array $streams = [])
45
    {
46 23
        $this->streams = [];
47 23
        $this->seekable = true;
48 23
        $this->current = 0;
49 23
        $this->pos = 0;
50 23
        $this->detached = false;
51
52 23
        foreach ($streams as $stream) {
53 7
            $this->addStream($stream);
54
        }
55 23
    }
56
57 5
    public function __toString()
58
    {
59
        try {
60 5
            $this->rewind();
61 5
            return $this->getContents();
62 1
        } catch (\Exception $e) {
63 1
            return '';
64
        }
65
    }
66
67
    /**
68
     * Add a stream to the AppendStream
69
     *
70
     * @param StreamInterface $stream Stream to append. Must be readable.
71
     *
72
     * @return $this
73
     * @throws \InvalidArgumentException if the stream is not readable
74
     */
75 17
    public function addStream(StreamInterface $stream) : self
76
    {
77 17
        if (false === $stream->isReadable()) {
78 1
            throw new \InvalidArgumentException('Each stream must be readable');
79
        }
80
81
        // The stream is only seekable if all streams are seekable
82 16
        if (false === $stream->isSeekable()) {
83
            $this->seekable = false;
84
        }
85
86 16
        $this->streams[] = $stream;
87
88 16
        return $this;
89
    }
90
91 5
    public function getContents()
92
    {
93 5
        return copy_to_string($this);
94
    }
95
96
    /**
97
     * Closes each attached stream.
98
     *
99
     * {@inheritdoc}
100
     */
101 3
    public function close()
102
    {
103 3
        $this->pos = $this->current = 0;
104
105 3
        foreach ($this->streams as $stream) {
106 2
            $stream->close();
107
        }
108
109 3
        $this->streams = [];
110 3
    }
111
112
    /**
113
     * Detaches each attached stream
114
     *
115
     * {@inheritdoc}
116
     */
117 2
    public function detach()
118
    {
119 2
        $this->close();
120
121 2
        $this->detached = true;
122 2
    }
123
124 2
    public function tell()
125
    {
126 2
        return $this->pos;
127
    }
128
129
    /**
130
     * Tries to calculate the size by adding the size of each stream.
131
     *
132
     * If any of the streams do not return a valid number, then the size of the
133
     * append stream cannot be determined and null is returned.
134
     *
135
     * {@inheritdoc}
136
     */
137 3
    public function getSize()
138
    {
139 3
        $size = 0;
140
141 3
        foreach ($this->streams as $stream) {
142 2
            $s = $stream->getSize();
143
144 2
            if (null === $s) {
145 1
                return null;
146
            }
147
148 2
            $size += $s;
149
        }
150
151 3
        return $size;
152
    }
153
154 11
    public function eof() : bool
155
    {
156 11
        return !$this->streams || ($this->current >= count($this->streams) - 1 &&
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->streams of type Psr\Http\Message\StreamInterface[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
157 11
            $this->streams[$this->current]->eof());
158
    }
159
160 5
    public function rewind()
161
    {
162 5
        $this->seek(0);
163 5
    }
164
165
    /**
166
     * Attempts to seek to the given position. Only supports SEEK_SET.
167
     *
168
     * {@inheritdoc}
169
     */
170 12
    public function seek($offset, $whence = SEEK_SET)
171
    {
172 12
        if (false === $this->seekable) {
173
            throw new \RuntimeException('This AppendStream is not seekable');
174 12
        } elseif ($whence !== SEEK_SET) {
175 1
            throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
176
        }
177
178 11
        $this->pos = $this->current = 0;
179
180
        // Rewind each stream
181 11
        foreach ($this->streams as $i => $stream) {
182
            try {
183 9
                $stream->rewind();
184 1
            } catch (\Exception $e) {
185 1
                throw new \RuntimeException(
186 1
                    'Unable to seek stream ' . $i . ' of the AppendStream',
187 9
                    0,
188
                    $e
189
                );
190
            }
191
        }
192
193
        // Seek to the actual position by reading from each stream
194 10
        while ($this->pos < $offset && false === $this->eof()) {
195 1
            $result = $this->read(min(8096, $offset - $this->pos));
196
197 1
            if ('' === $result) {
198
                break;
199
            }
200
        }
201 10
    }
202
203
    /**
204
     * Reads from all of the appended streams until the length is met or EOF.
205
     *
206
     * {@inheritdoc}
207
     */
208 9
    public function read($length)
209
    {
210 9
        $buffer = '';
211 9
        $total = count($this->streams) - 1;
212 9
        $remaining = $length;
213 9
        $progressToNext = false;
214
215 9
        while (0 < $remaining) {
216
            // Progress to the next stream if needed.
217 9
            if ($progressToNext || $this->streams[$this->current]->eof()) {
218 8
                $progressToNext = false;
219
220 8
                if ($total === $this->current) {
221 7
                    break;
222
                }
223
224 7
                $this->current++;
225
            }
226
227 9
            $result = $this->streams[$this->current]->read($remaining);
228
229
            // Using a loose comparison here to match on '', false, and null
230 8
            if (null === $result) {
231
                $progressToNext = true;
232
233
                continue;
234
            }
235
236 8
            $buffer .= $result;
237 8
            $remaining = $length - strlen($buffer);
238
        }
239
240 8
        $this->pos += strlen($buffer);
241
242 8
        return $buffer;
243
    }
244
245 1
    public function isReadable() : bool
246
    {
247 1
        return true;
248
    }
249
250 1
    public function isWritable() : bool
251
    {
252 1
        return false;
253
    }
254
255 5
    public function isSeekable() : bool
256
    {
257 5
        return $this->seekable;
258
    }
259
260 1
    public function write($string)
261
    {
262 1
        throw new \RuntimeException('Cannot write to an AppendStream');
263
    }
264
265 1
    public function getMetadata($key = null)
266
    {
267 1
        return $key ? null : [];
268
    }
269
}
270