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