AppendStream.php 5.4 KB

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