1<?php
2
3namespace GuzzleHttp\Psr7;
4
5use Psr\Http\Message\StreamInterface;
6
7/**
8 * Reads from multiple streams, one after the other.
9 *
10 * This is a read-only stream decorator.
11 */
12class AppendStream implements StreamInterface
13{
14    /** @var StreamInterface[] Streams being decorated */
15    private $streams = [];
16
17    private $seekable = true;
18    private $current = 0;
19    private $pos = 0;
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 Utils::copyToString($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        $this->seekable = true;
77
78        foreach ($this->streams as $stream) {
79            $stream->close();
80        }
81
82        $this->streams = [];
83    }
84
85    /**
86     * Detaches each attached stream.
87     *
88     * Returns null as it's not clear which underlying stream resource to return.
89     *
90     * {@inheritdoc}
91     */
92    public function detach()
93    {
94        $this->pos = $this->current = 0;
95        $this->seekable = true;
96
97        foreach ($this->streams as $stream) {
98            $stream->detach();
99        }
100
101        $this->streams = [];
102
103        return null;
104    }
105
106    public function tell()
107    {
108        return $this->pos;
109    }
110
111    /**
112     * Tries to calculate the size by adding the size of each stream.
113     *
114     * If any of the streams do not return a valid number, then the size of the
115     * append stream cannot be determined and null is returned.
116     *
117     * {@inheritdoc}
118     */
119    public function getSize()
120    {
121        $size = 0;
122
123        foreach ($this->streams as $stream) {
124            $s = $stream->getSize();
125            if ($s === null) {
126                return null;
127            }
128            $size += $s;
129        }
130
131        return $size;
132    }
133
134    public function eof()
135    {
136        return !$this->streams ||
137            ($this->current >= count($this->streams) - 1 &&
138             $this->streams[$this->current]->eof());
139    }
140
141    public function rewind()
142    {
143        $this->seek(0);
144    }
145
146    /**
147     * Attempts to seek to the given position. Only supports SEEK_SET.
148     *
149     * {@inheritdoc}
150     */
151    public function seek($offset, $whence = SEEK_SET)
152    {
153        if (!$this->seekable) {
154            throw new \RuntimeException('This AppendStream is not seekable');
155        } elseif ($whence !== SEEK_SET) {
156            throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
157        }
158
159        $this->pos = $this->current = 0;
160
161        // Rewind each stream
162        foreach ($this->streams as $i => $stream) {
163            try {
164                $stream->rewind();
165            } catch (\Exception $e) {
166                throw new \RuntimeException('Unable to seek stream '
167                    . $i . ' of the AppendStream', 0, $e);
168            }
169        }
170
171        // Seek to the actual position by reading from each stream
172        while ($this->pos < $offset && !$this->eof()) {
173            $result = $this->read(min(8096, $offset - $this->pos));
174            if ($result === '') {
175                break;
176            }
177        }
178    }
179
180    /**
181     * Reads from all of the appended streams until the length is met or EOF.
182     *
183     * {@inheritdoc}
184     */
185    public function read($length)
186    {
187        $buffer = '';
188        $total = count($this->streams) - 1;
189        $remaining = $length;
190        $progressToNext = false;
191
192        while ($remaining > 0) {
193
194            // Progress to the next stream if needed.
195            if ($progressToNext || $this->streams[$this->current]->eof()) {
196                $progressToNext = false;
197                if ($this->current === $total) {
198                    break;
199                }
200                $this->current++;
201            }
202
203            $result = $this->streams[$this->current]->read($remaining);
204
205            // Using a loose comparison here to match on '', false, and null
206            if ($result == null) {
207                $progressToNext = true;
208                continue;
209            }
210
211            $buffer .= $result;
212            $remaining = $length - strlen($buffer);
213        }
214
215        $this->pos += strlen($buffer);
216
217        return $buffer;
218    }
219
220    public function isReadable()
221    {
222        return true;
223    }
224
225    public function isWritable()
226    {
227        return false;
228    }
229
230    public function isSeekable()
231    {
232        return $this->seekable;
233    }
234
235    public function write($string)
236    {
237        throw new \RuntimeException('Cannot write to an AppendStream');
238    }
239
240    public function getMetadata($key = null)
241    {
242        return $key ? null : [];
243    }
244}
245