2 namespace GuzzleHttp\Psr7;
4 use Psr\Http\Message\StreamInterface;
7 * Reads from multiple streams, one after the other.
9 * This is a read-only stream decorator.
11 class AppendStream implements StreamInterface
13 /** @var StreamInterface[] Streams being decorated */
14 private $streams = [];
16 private $seekable = true;
19 private $detached = false;
22 * @param StreamInterface[] $streams Streams to decorate. Each stream must
25 public function __construct(array $streams = [])
27 foreach ($streams as $stream) {
28 $this->addStream($stream);
32 public function __toString()
36 return $this->getContents();
37 } catch (\Exception $e) {
43 * Add a stream to the AppendStream
45 * @param StreamInterface $stream Stream to append. Must be readable.
47 * @throws \InvalidArgumentException if the stream is not readable
49 public function addStream(StreamInterface $stream)
51 if (!$stream->isReadable()) {
52 throw new \InvalidArgumentException('Each stream must be readable');
55 // The stream is only seekable if all streams are seekable
56 if (!$stream->isSeekable()) {
57 $this->seekable = false;
60 $this->streams[] = $stream;
63 public function getContents()
65 return copy_to_string($this);
69 * Closes each attached stream.
73 public function close()
75 $this->pos = $this->current = 0;
77 foreach ($this->streams as $stream) {
85 * Detaches each attached stream
89 public function detach()
92 $this->detached = true;
95 public function tell()
101 * Tries to calculate the size by adding the size of each stream.
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.
108 public function getSize()
112 foreach ($this->streams as $stream) {
113 $s = $stream->getSize();
123 public function eof()
125 return !$this->streams ||
126 ($this->current >= count($this->streams) - 1 &&
127 $this->streams[$this->current]->eof());
130 public function rewind()
136 * Attempts to seek to the given position. Only supports SEEK_SET.
140 public function seek($offset, $whence = SEEK_SET)
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');
148 $this->pos = $this->current = 0;
150 // Rewind each stream
151 foreach ($this->streams as $i => $stream) {
154 } catch (\Exception $e) {
155 throw new \RuntimeException('Unable to seek stream '
156 . $i . ' of the AppendStream', 0, $e);
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 === '') {
170 * Reads from all of the appended streams until the length is met or EOF.
174 public function read($length)
177 $total = count($this->streams) - 1;
178 $remaining = $length;
179 $progressToNext = false;
181 while ($remaining > 0) {
183 // Progress to the next stream if needed.
184 if ($progressToNext || $this->streams[$this->current]->eof()) {
185 $progressToNext = false;
186 if ($this->current === $total) {
192 $result = $this->streams[$this->current]->read($remaining);
194 // Using a loose comparison here to match on '', false, and null
195 if ($result == null) {
196 $progressToNext = true;
201 $remaining = $length - strlen($buffer);
204 $this->pos += strlen($buffer);
209 public function isReadable()
214 public function isWritable()
219 public function isSeekable()
221 return $this->seekable;
224 public function write($string)
226 throw new \RuntimeException('Cannot write to an AppendStream');
229 public function getMetadata($key = null)
231 return $key ? null : [];