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