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