StandardStream.php 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * This file is part of Hyperf.
  5. *
  6. * @link https://www.hyperf.io
  7. * @document https://hyperf.wiki
  8. * @contact group@hyperf.io
  9. * @license https://github.com/hyperf/hyperf/blob/master/LICENSE
  10. */
  11. namespace Hyperf\HttpMessage\Stream;
  12. use Exception;
  13. use InvalidArgumentException;
  14. use Psr\Http\Message\StreamInterface;
  15. use RuntimeException;
  16. use function clearstatcache;
  17. use function fclose;
  18. use function feof;
  19. use function fopen;
  20. use function fread;
  21. use function fseek;
  22. use function fstat;
  23. use function ftell;
  24. use function fwrite;
  25. use function is_resource;
  26. use function is_string;
  27. use function stream_get_contents;
  28. use function stream_get_meta_data;
  29. use function var_export;
  30. use const SEEK_CUR;
  31. use const SEEK_SET;
  32. /**
  33. * Code Taken from Nyholm/psr7.
  34. * Author: Michael Dowling and contributors to guzzlehttp/psr7
  35. * Author: Tobias Nyholm <tobias.nyholm@gmail.com>
  36. * Author: Martijn van der Ven <martijn@vanderven.se>.
  37. * @license https://github.com/Nyholm/psr7/blob/master/LICENSE
  38. */
  39. final class StandardStream implements StreamInterface
  40. {
  41. /** @var array Hash of readable and writable stream types */
  42. private const READ_WRITE_HASH = [
  43. 'read' => [
  44. 'r' => true, 'w+' => true, 'r+' => true, 'x+' => true, 'c+' => true,
  45. 'rb' => true, 'w+b' => true, 'r+b' => true, 'x+b' => true,
  46. 'c+b' => true, 'rt' => true, 'w+t' => true, 'r+t' => true,
  47. 'x+t' => true, 'c+t' => true, 'a+' => true,
  48. ],
  49. 'write' => [
  50. 'w' => true, 'w+' => true, 'rw' => true, 'r+' => true, 'x+' => true,
  51. 'c+' => true, 'wb' => true, 'w+b' => true, 'r+b' => true,
  52. 'x+b' => true, 'c+b' => true, 'w+t' => true, 'r+t' => true,
  53. 'x+t' => true, 'c+t' => true, 'a' => true, 'a+' => true,
  54. ],
  55. ];
  56. /** @var null|resource A resource reference */
  57. private $stream;
  58. /** @var bool */
  59. private $seekable;
  60. /** @var bool */
  61. private $readable;
  62. /** @var bool */
  63. private $writable;
  64. /** @var null|array|mixed|void */
  65. private $uri;
  66. /** @var null|int */
  67. private $size;
  68. private function __construct()
  69. {
  70. }
  71. /**
  72. * Closes the stream when the destructed.
  73. */
  74. public function __destruct()
  75. {
  76. $this->close();
  77. }
  78. public function __toString(): string
  79. {
  80. try {
  81. if ($this->isSeekable()) {
  82. $this->seek(0);
  83. }
  84. return $this->getContents();
  85. } catch (Exception $e) {
  86. return '';
  87. }
  88. }
  89. /**
  90. * Creates a new PSR-7 stream.
  91. *
  92. * @param resource|StreamInterface|string $body
  93. *
  94. * @throws InvalidArgumentException
  95. */
  96. public static function create($body = ''): StreamInterface
  97. {
  98. if ($body instanceof StreamInterface) {
  99. return $body;
  100. }
  101. if (is_string($body)) {
  102. $resource = fopen('php://temp', 'rw+');
  103. fwrite($resource, $body);
  104. $body = $resource;
  105. }
  106. if (is_resource($body)) {
  107. $new = new self();
  108. $new->stream = $body;
  109. $meta = stream_get_meta_data($new->stream);
  110. $new->seekable = $meta['seekable'] && fseek($new->stream, 0, SEEK_CUR) === 0;
  111. $new->readable = isset(self::READ_WRITE_HASH['read'][$meta['mode']]);
  112. $new->writable = isset(self::READ_WRITE_HASH['write'][$meta['mode']]);
  113. $new->uri = $new->getMetadata('uri');
  114. return $new;
  115. }
  116. throw new InvalidArgumentException('First argument to Stream::create() must be a string, resource or StreamInterface.');
  117. }
  118. public function close(): void
  119. {
  120. if (isset($this->stream)) {
  121. if (is_resource($this->stream)) {
  122. fclose($this->stream);
  123. }
  124. $this->detach();
  125. }
  126. }
  127. public function detach()
  128. {
  129. if (! isset($this->stream)) {
  130. return null;
  131. }
  132. $result = $this->stream;
  133. unset($this->stream);
  134. $this->size = $this->uri = null;
  135. $this->readable = $this->writable = $this->seekable = false;
  136. return $result;
  137. }
  138. public function getSize(): ?int
  139. {
  140. if ($this->size !== null) {
  141. return $this->size;
  142. }
  143. if (! isset($this->stream)) {
  144. return null;
  145. }
  146. // Clear the stat cache if the stream has a URI
  147. if ($this->uri) {
  148. clearstatcache(true, $this->uri);
  149. }
  150. $stats = fstat($this->stream);
  151. if (isset($stats['size'])) {
  152. $this->size = $stats['size'];
  153. return $this->size;
  154. }
  155. return null;
  156. }
  157. public function tell(): int
  158. {
  159. if (false === $result = ftell($this->stream)) {
  160. throw new RuntimeException('Unable to determine stream position');
  161. }
  162. return $result;
  163. }
  164. public function eof(): bool
  165. {
  166. return ! $this->stream || feof($this->stream);
  167. }
  168. public function isSeekable(): bool
  169. {
  170. return $this->seekable;
  171. }
  172. public function seek($offset, $whence = SEEK_SET): void
  173. {
  174. if (! $this->seekable) {
  175. throw new RuntimeException('Stream is not seekable');
  176. }
  177. if (fseek($this->stream, $offset, $whence) === -1) {
  178. throw new RuntimeException('Unable to seek to stream position ' . $offset . ' with whence ' . var_export($whence, true));
  179. }
  180. }
  181. public function rewind(): void
  182. {
  183. $this->seek(0);
  184. }
  185. public function isWritable(): bool
  186. {
  187. return $this->writable;
  188. }
  189. public function write($string): int
  190. {
  191. if (! $this->writable) {
  192. throw new RuntimeException('Cannot write to a non-writable stream');
  193. }
  194. // We can't know the size after writing anything
  195. $this->size = null;
  196. if (false === $result = fwrite($this->stream, $string)) {
  197. throw new RuntimeException('Unable to write to stream');
  198. }
  199. return $result;
  200. }
  201. public function isReadable(): bool
  202. {
  203. return $this->readable;
  204. }
  205. public function read($length): string
  206. {
  207. if (! $this->readable) {
  208. throw new RuntimeException('Cannot read from non-readable stream');
  209. }
  210. return fread($this->stream, $length);
  211. }
  212. public function getContents(): string
  213. {
  214. if (! isset($this->stream)) {
  215. throw new RuntimeException('Unable to read stream contents');
  216. }
  217. if (false === $contents = stream_get_contents($this->stream)) {
  218. throw new RuntimeException('Unable to read stream contents');
  219. }
  220. return $contents;
  221. }
  222. public function getMetadata($key = null)
  223. {
  224. if (! isset($this->stream)) {
  225. return $key ? null : [];
  226. }
  227. $meta = stream_get_meta_data($this->stream);
  228. if ($key === null) {
  229. return $meta;
  230. }
  231. return $meta[$key] ?? null;
  232. }
  233. }