AppendStream.php 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242
  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 copy_to_string($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. }
  87. public function tell()
  88. {
  89. return $this->pos;
  90. }
  91. /**
  92. * Tries to calculate the size by adding the size of each stream.
  93. *
  94. * If any of the streams do not return a valid number, then the size of the
  95. * append stream cannot be determined and null is returned.
  96. *
  97. * {@inheritdoc}
  98. */
  99. public function getSize()
  100. {
  101. $size = 0;
  102. foreach ($this->streams as $stream) {
  103. $s = $stream->getSize();
  104. if ($s === null) {
  105. return null;
  106. }
  107. $size += $s;
  108. }
  109. return $size;
  110. }
  111. public function eof()
  112. {
  113. return !$this->streams ||
  114. ($this->current >= count($this->streams) - 1 &&
  115. $this->streams[$this->current]->eof());
  116. }
  117. public function rewind()
  118. {
  119. $this->seek(0);
  120. }
  121. /**
  122. * Attempts to seek to the given position. Only supports SEEK_SET.
  123. *
  124. * {@inheritdoc}
  125. */
  126. public function seek($offset, $whence = SEEK_SET)
  127. {
  128. if (!$this->seekable) {
  129. throw new \RuntimeException('This AppendStream is not seekable');
  130. } elseif ($whence !== SEEK_SET) {
  131. throw new \RuntimeException('The AppendStream can only seek with SEEK_SET');
  132. }
  133. $this->pos = $this->current = 0;
  134. // Rewind each stream
  135. foreach ($this->streams as $i => $stream) {
  136. try {
  137. $stream->rewind();
  138. } catch (\Exception $e) {
  139. throw new \RuntimeException('Unable to seek stream '
  140. . $i . ' of the AppendStream', 0, $e);
  141. }
  142. }
  143. // Seek to the actual position by reading from each stream
  144. while ($this->pos < $offset && !$this->eof()) {
  145. $result = $this->read(min(8096, $offset - $this->pos));
  146. if ($result === '') {
  147. break;
  148. }
  149. }
  150. }
  151. /**
  152. * Reads from all of the appended streams until the length is met or EOF.
  153. *
  154. * {@inheritdoc}
  155. */
  156. public function read($length)
  157. {
  158. $buffer = '';
  159. $total = count($this->streams) - 1;
  160. $remaining = $length;
  161. $progressToNext = false;
  162. while ($remaining > 0) {
  163. // Progress to the next stream if needed.
  164. if ($progressToNext || $this->streams[$this->current]->eof()) {
  165. $progressToNext = false;
  166. if ($this->current === $total) {
  167. break;
  168. }
  169. $this->current++;
  170. }
  171. $result = $this->streams[$this->current]->read($remaining);
  172. // Using a loose comparison here to match on '', false, and null
  173. if ($result == null) {
  174. $progressToNext = true;
  175. continue;
  176. }
  177. $buffer .= $result;
  178. $remaining = $length - strlen($buffer);
  179. }
  180. $this->pos += strlen($buffer);
  181. return $buffer;
  182. }
  183. public function isReadable()
  184. {
  185. return true;
  186. }
  187. public function isWritable()
  188. {
  189. return false;
  190. }
  191. public function isSeekable()
  192. {
  193. return $this->seekable;
  194. }
  195. public function write($string)
  196. {
  197. throw new \RuntimeException('Cannot write to an AppendStream');
  198. }
  199. public function getMetadata($key = null)
  200. {
  201. return $key ? null : [];
  202. }
  203. }