Completed
Push — master ( 8d12ff...458df2 )
by Guillaume
02:56
created

AppendStream   B

Complexity

Total Complexity 39

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 1

Importance

Changes 0
Metric Value
dl 0
loc 223
rs 8.2857
c 0
b 0
f 0
wmc 39
lcom 1
cbo 1

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 2
A __toString() 0 9 2
A addStream() 0 13 3
A getContents() 0 4 1
A close() 0 10 2
A detach() 0 5 1
A tell() 0 4 1
A getSize() 0 14 3
A eof() 0 6 3
A rewind() 0 4 1
C seek() 0 28 8
B read() 0 34 6
A isReadable() 0 4 1
A isWritable() 0 4 1
A isSeekable() 0 4 1
A write() 0 4 1
A getMetadata() 0 4 2
1
<?php
2
namespace GuzzleHttp\Psr7;
3
4
use Psr\Http\Message\StreamInterface;
5
6
/**
7
 * Reads from multiple streams, one after the other.
8
 *
9
 * This is a read-only stream decorator.
10
 */
11
class AppendStream implements StreamInterface
12
{
13
    /** @var StreamInterface[] Streams being decorated */
14
    private $streams = [];
15
16
    private $seekable = true;
17
    private $current = 0;
18
    private $pos = 0;
19
    private $detached = false;
20
21
    /**
22
     * @param StreamInterface[] $streams Streams to decorate. Each stream must
23
     *                                   be readable.
24
     */
25
    public function __construct(array $streams = [])
26
    {
27
        foreach ($streams as $stream) {
28
            $this->addStream($stream);
29
        }
30
    }
31
32
    public function __toString()
33
    {
34
        try {
35
            $this->rewind();
36
            return $this->getContents();
37
        } catch (\Exception $e) {
38
            return '';
39
        }
40
    }
41
42
    /**
43
     * Add a stream to the AppendStream
44
     *
45
     * @param StreamInterface $stream Stream to append. Must be readable.
46
     *
47
     * @throws \InvalidArgumentException if the stream is not readable
48
     */
49
    public function addStream(StreamInterface $stream)
50
    {
51
        if (!$stream->isReadable()) {
52
            throw new \InvalidArgumentException('Each stream must be readable');
53
        }
54
55
        // The stream is only seekable if all streams are seekable
56
        if (!$stream->isSeekable()) {
57
            $this->seekable = false;
58
        }
59
60
        $this->streams[] = $stream;
61
    }
62
63
    public function getContents()
64
    {
65
        return copy_to_string($this);
66
    }
67
68
    /**
69
     * Closes each attached stream.
70
     *
71
     * {@inheritdoc}
72
     */
73
    public function close()
74
    {
75
        $this->pos = $this->current = 0;
76
77
        foreach ($this->streams as $stream) {
78
            $stream->close();
79
        }
80
81
        $this->streams = [];
82
    }
83
84
    /**
85
     * Detaches each attached stream
86
     *
87
     * {@inheritdoc}
88
     */
89
    public function detach()
90
    {
91
        $this->close();
92
        $this->detached = true;
93
    }
94
95
    public function tell()
96
    {
97
        return $this->pos;
98
    }
99
100
    /**
101
     * Tries to calculate the size by adding the size of each stream.
102
     *
103
     * If any of the streams do not return a valid number, then the size of the
104
     * append stream cannot be determined and null is returned.
105
     *
106
     * {@inheritdoc}
107
     */
108
    public function getSize()
109
    {
110
        $size = 0;
111
112
        foreach ($this->streams as $stream) {
113
            $s = $stream->getSize();
114
            if ($s === null) {
115
                return null;
116
            }
117
            $size += $s;
118
        }
119
120
        return $size;
121
    }
122
123
    public function eof()
124
    {
125
        return !$this->streams ||
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...
126
            ($this->current >= count($this->streams) - 1 &&
127
             $this->streams[$this->current]->eof());
128
    }
129
130
    public function rewind()
131
    {
132
        $this->seek(0);
133
    }
134
135
    /**
136
     * Attempts to seek to the given position. Only supports SEEK_SET.
137
     *
138
     * {@inheritdoc}
139
     */
140
    public function seek($offset, $whence = SEEK_SET)
141
    {
142
        if (!$this->seekable) {
143
            throw new \RuntimeException('This AppendStream is not seekable');
144
        } elseif ($whence !== SEEK_SET) {
145
            throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
146
        }
147
148
        $this->pos = $this->current = 0;
149
150
        // Rewind each stream
151
        foreach ($this->streams as $i => $stream) {
152
            try {
153
                $stream->rewind();
154
            } catch (\Exception $e) {
155
                throw new \RuntimeException('Unable to seek stream '
156
                    . $i . ' of the AppendStream', 0, $e);
157
            }
158
        }
159
160
        // Seek to the actual position by reading from each stream
161
        while ($this->pos < $offset && !$this->eof()) {
162
            $result = $this->read(min(8096, $offset - $this->pos));
163
            if ($result === '') {
164
                break;
165
            }
166
        }
167
    }
168
169
    /**
170
     * Reads from all of the appended streams until the length is met or EOF.
171
     *
172
     * {@inheritdoc}
173
     */
174
    public function read($length)
175
    {
176
        $buffer = '';
177
        $total = count($this->streams) - 1;
178
        $remaining = $length;
179
        $progressToNext = false;
180
181
        while ($remaining > 0) {
182
183
            // Progress to the next stream if needed.
184
            if ($progressToNext || $this->streams[$this->current]->eof()) {
185
                $progressToNext = false;
186
                if ($this->current === $total) {
187
                    break;
188
                }
189
                $this->current++;
190
            }
191
192
            $result = $this->streams[$this->current]->read($remaining);
193
194
            // Using a loose comparison here to match on '', false, and null
195
            if ($result == null) {
196
                $progressToNext = true;
197
                continue;
198
            }
199
200
            $buffer .= $result;
201
            $remaining = $length - strlen($buffer);
202
        }
203
204
        $this->pos += strlen($buffer);
205
206
        return $buffer;
207
    }
208
209
    public function isReadable()
210
    {
211
        return true;
212
    }
213
214
    public function isWritable()
215
    {
216
        return false;
217
    }
218
219
    public function isSeekable()
220
    {
221
        return $this->seekable;
222
    }
223
224
    public function write($string)
225
    {
226
        throw new \RuntimeException('Cannot write to an AppendStream');
227
    }
228
229
    public function getMetadata($key = null)
230
    {
231
        return $key ? null : [];
232
    }
233
}
234