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