AppendStream.php 5.6 KB

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