1<?php
2namespace GuzzleHttp\Stream;
3
4use GuzzleHttp\Stream\Exception\CannotAttachException;
5
6/**
7 * Reads from multiple streams, one after the other.
8 *
9 * This is a read-only stream decorator.
10 */
11class 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->seek(0);
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
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 attach($stream)
96    {
97        throw new CannotAttachException();
98    }
99
100    public function tell()
101    {
102        return $this->pos;
103    }
104
105    /**
106     * Tries to calculate the size by adding the size of each stream.
107     *
108     * If any of the streams do not return a valid number, then the size of the
109     * append stream cannot be determined and null is returned.
110     *
111     * {@inheritdoc}
112     */
113    public function getSize()
114    {
115        $size = 0;
116
117        foreach ($this->streams as $stream) {
118            $s = $stream->getSize();
119            if ($s === null) {
120                return null;
121            }
122            $size += $s;
123        }
124
125        return $size;
126    }
127
128    public function eof()
129    {
130        return !$this->streams ||
131            ($this->current >= count($this->streams) - 1 &&
132             $this->streams[$this->current]->eof());
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 || $whence !== SEEK_SET) {
143            return false;
144        }
145
146        $success = true;
147        $this->pos = $this->current = 0;
148
149        // Rewind each stream
150        foreach ($this->streams as $stream) {
151            if (!$stream->seek(0)) {
152                $success = false;
153            }
154        }
155
156        if (!$success) {
157            return false;
158        }
159
160        // Seek to the actual position by reading from each stream
161        while ($this->pos < $offset && !$this->eof()) {
162            $this->read(min(8096, $offset - $this->pos));
163        }
164
165        return $this->pos == $offset;
166    }
167
168    /**
169     * Reads from all of the appended streams until the length is met or EOF.
170     *
171     * {@inheritdoc}
172     */
173    public function read($length)
174    {
175        $buffer = '';
176        $total = count($this->streams) - 1;
177        $remaining = $length;
178
179        while ($remaining > 0) {
180            // Progress to the next stream if needed.
181            if ($this->streams[$this->current]->eof()) {
182                if ($this->current == $total) {
183                    break;
184                }
185                $this->current++;
186            }
187            $buffer .= $this->streams[$this->current]->read($remaining);
188            $remaining = $length - strlen($buffer);
189        }
190
191        $this->pos += strlen($buffer);
192
193        return $buffer;
194    }
195
196    public function isReadable()
197    {
198        return true;
199    }
200
201    public function isWritable()
202    {
203        return false;
204    }
205
206    public function isSeekable()
207    {
208        return $this->seekable;
209    }
210
211    public function write($string)
212    {
213        return false;
214    }
215
216    public function getMetadata($key = null)
217    {
218        return $key ? null : [];
219    }
220}
221