Stream.php 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. <?php
  2. namespace GuzzleHttp\Psr7;
  3. use Psr\Http\Message\StreamInterface;
  4. /**
  5. * PHP stream implementation.
  6. *
  7. * @var $stream
  8. */
  9. class Stream implements StreamInterface
  10. {
  11. private $stream;
  12. private $size;
  13. private $seekable;
  14. private $readable;
  15. private $writable;
  16. private $uri;
  17. private $customMetadata;
  18. /** @var array Hash of readable and writable stream types */
  19. private static $readWriteHash = [
  20. 'read' => [
  21. 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
  22. 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
  23. 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
  24. 'x+t' => true, 'c+t' => true, 'a+' => true, 'rb+' => true,
  25. ],
  26. 'write' => [
  27. 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
  28. 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true, 'rb+' => true,
  29. 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
  30. 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true
  31. ]
  32. ];
  33. /**
  34. * This constructor accepts an associative array of options.
  35. *
  36. * - size: (int) If a read stream would otherwise have an indeterminate
  37. * size, but the size is known due to foreknowledge, then you can
  38. * provide that size, in bytes.
  39. * - metadata: (array) Any additional metadata to return when the metadata
  40. * of the stream is accessed.
  41. *
  42. * @param resource $stream Stream resource to wrap.
  43. * @param array $options Associative array of options.
  44. *
  45. * @throws \InvalidArgumentException if the stream is not a stream resource
  46. */
  47. public function __construct($stream, $options = [])
  48. {
  49. if (!is_resource($stream)) {
  50. throw new \InvalidArgumentException('Stream must be a resource');
  51. }
  52. if (isset($options['size'])) {
  53. $this->size = $options['size'];
  54. }
  55. $this->customMetadata = isset($options['metadata'])
  56. ? $options['metadata']
  57. : [];
  58. $this->stream = $stream;
  59. $meta = stream_get_meta_data($this->stream);
  60. $this->seekable = $meta['seekable'];
  61. $this->readable = isset(self::$readWriteHash['read'][$meta['mode']]);
  62. $this->writable = isset(self::$readWriteHash['write'][$meta['mode']]);
  63. $this->uri = $this->getMetadata('uri');
  64. }
  65. /**
  66. * Closes the stream when the destructed
  67. */
  68. public function __destruct()
  69. {
  70. $this->close();
  71. }
  72. public function __toString()
  73. {
  74. try {
  75. $this->seek(0);
  76. return (string) stream_get_contents($this->stream);
  77. } catch (\Exception $e) {
  78. return '';
  79. }
  80. }
  81. public function getContents()
  82. {
  83. if (!isset($this->stream)) {
  84. throw new \RuntimeException('Stream is detached');
  85. }
  86. $contents = stream_get_contents($this->stream);
  87. if ($contents === false) {
  88. throw new \RuntimeException('Unable to read stream contents');
  89. }
  90. return $contents;
  91. }
  92. public function close()
  93. {
  94. if (isset($this->stream)) {
  95. if (is_resource($this->stream)) {
  96. fclose($this->stream);
  97. }
  98. $this->detach();
  99. }
  100. }
  101. public function detach()
  102. {
  103. if (!isset($this->stream)) {
  104. return null;
  105. }
  106. $result = $this->stream;
  107. unset($this->stream);
  108. $this->size = $this->uri = null;
  109. $this->readable = $this->writable = $this->seekable = false;
  110. return $result;
  111. }
  112. public function getSize()
  113. {
  114. if ($this->size !== null) {
  115. return $this->size;
  116. }
  117. if (!isset($this->stream)) {
  118. return null;
  119. }
  120. // Clear the stat cache if the stream has a URI
  121. if ($this->uri) {
  122. clearstatcache(true, $this->uri);
  123. }
  124. $stats = fstat($this->stream);
  125. if (isset($stats['size'])) {
  126. $this->size = $stats['size'];
  127. return $this->size;
  128. }
  129. return null;
  130. }
  131. public function isReadable()
  132. {
  133. return $this->readable;
  134. }
  135. public function isWritable()
  136. {
  137. return $this->writable;
  138. }
  139. public function isSeekable()
  140. {
  141. return $this->seekable;
  142. }
  143. public function eof()
  144. {
  145. if (!isset($this->stream)) {
  146. throw new \RuntimeException('Stream is detached');
  147. }
  148. return feof($this->stream);
  149. }
  150. public function tell()
  151. {
  152. if (!isset($this->stream)) {
  153. throw new \RuntimeException('Stream is detached');
  154. }
  155. $result = ftell($this->stream);
  156. if ($result === false) {
  157. throw new \RuntimeException('Unable to determine stream position');
  158. }
  159. return $result;
  160. }
  161. public function rewind()
  162. {
  163. $this->seek(0);
  164. }
  165. public function seek($offset, $whence = SEEK_SET)
  166. {
  167. if (!isset($this->stream)) {
  168. throw new \RuntimeException('Stream is detached');
  169. }
  170. if (!$this->seekable) {
  171. throw new \RuntimeException('Stream is not seekable');
  172. }
  173. if (fseek($this->stream, $offset, $whence) === -1) {
  174. throw new \RuntimeException('Unable to seek to stream position '
  175. . $offset . ' with whence ' . var_export($whence, true));
  176. }
  177. }
  178. public function read($length)
  179. {
  180. if (!isset($this->stream)) {
  181. throw new \RuntimeException('Stream is detached');
  182. }
  183. if (!$this->readable) {
  184. throw new \RuntimeException('Cannot read from non-readable stream');
  185. }
  186. if ($length < 0) {
  187. throw new \RuntimeException('Length parameter cannot be negative');
  188. }
  189. if (0 === $length) {
  190. return '';
  191. }
  192. $string = fread($this->stream, $length);
  193. if (false === $string) {
  194. throw new \RuntimeException('Unable to read from stream');
  195. }
  196. return $string;
  197. }
  198. public function write($string)
  199. {
  200. if (!isset($this->stream)) {
  201. throw new \RuntimeException('Stream is detached');
  202. }
  203. if (!$this->writable) {
  204. throw new \RuntimeException('Cannot write to a non-writable stream');
  205. }
  206. // We can't know the size after writing anything
  207. $this->size = null;
  208. $result = fwrite($this->stream, $string);
  209. if ($result === false) {
  210. throw new \RuntimeException('Unable to write to stream');
  211. }
  212. return $result;
  213. }
  214. public function getMetadata($key = null)
  215. {
  216. if (!isset($this->stream)) {
  217. return $key ? null : [];
  218. } elseif (!$key) {
  219. return $this->customMetadata + stream_get_meta_data($this->stream);
  220. } elseif (isset($this->customMetadata[$key])) {
  221. return $this->customMetadata[$key];
  222. }
  223. $meta = stream_get_meta_data($this->stream);
  224. return isset($meta[$key]) ? $meta[$key] : null;
  225. }
  226. }