AppendStream.php 5.8 KB

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