1<?php 2 3namespace GuzzleHttp\Psr7; 4 5use Psr\Http\Message\StreamInterface; 6 7/** 8 * PHP stream implementation. 9 * 10 * @var $stream 11 */ 12class Stream implements StreamInterface 13{ 14 /** 15 * Resource modes. 16 * 17 * @var string 18 * 19 * @see http://php.net/manual/function.fopen.php 20 * @see http://php.net/manual/en/function.gzopen.php 21 */ 22 const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/'; 23 const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/'; 24 25 private $stream; 26 private $size; 27 private $seekable; 28 private $readable; 29 private $writable; 30 private $uri; 31 private $customMetadata; 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 53 if (isset($options['size'])) { 54 $this->size = $options['size']; 55 } 56 57 $this->customMetadata = isset($options['metadata']) 58 ? $options['metadata'] 59 : []; 60 61 $this->stream = $stream; 62 $meta = stream_get_meta_data($this->stream); 63 $this->seekable = $meta['seekable']; 64 $this->readable = (bool)preg_match(self::READABLE_MODES, $meta['mode']); 65 $this->writable = (bool)preg_match(self::WRITABLE_MODES, $meta['mode']); 66 $this->uri = $this->getMetadata('uri'); 67 } 68 69 /** 70 * Closes the stream when the destructed 71 */ 72 public function __destruct() 73 { 74 $this->close(); 75 } 76 77 public function __toString() 78 { 79 try { 80 if ($this->isSeekable()) { 81 $this->seek(0); 82 } 83 return $this->getContents(); 84 } catch (\Exception $e) { 85 return ''; 86 } 87 } 88 89 public function getContents() 90 { 91 if (!isset($this->stream)) { 92 throw new \RuntimeException('Stream is detached'); 93 } 94 95 $contents = stream_get_contents($this->stream); 96 97 if ($contents === false) { 98 throw new \RuntimeException('Unable to read stream contents'); 99 } 100 101 return $contents; 102 } 103 104 public function close() 105 { 106 if (isset($this->stream)) { 107 if (is_resource($this->stream)) { 108 fclose($this->stream); 109 } 110 $this->detach(); 111 } 112 } 113 114 public function detach() 115 { 116 if (!isset($this->stream)) { 117 return null; 118 } 119 120 $result = $this->stream; 121 unset($this->stream); 122 $this->size = $this->uri = null; 123 $this->readable = $this->writable = $this->seekable = false; 124 125 return $result; 126 } 127 128 public function getSize() 129 { 130 if ($this->size !== null) { 131 return $this->size; 132 } 133 134 if (!isset($this->stream)) { 135 return null; 136 } 137 138 // Clear the stat cache if the stream has a URI 139 if ($this->uri) { 140 clearstatcache(true, $this->uri); 141 } 142 143 $stats = fstat($this->stream); 144 if (isset($stats['size'])) { 145 $this->size = $stats['size']; 146 return $this->size; 147 } 148 149 return null; 150 } 151 152 public function isReadable() 153 { 154 return $this->readable; 155 } 156 157 public function isWritable() 158 { 159 return $this->writable; 160 } 161 162 public function isSeekable() 163 { 164 return $this->seekable; 165 } 166 167 public function eof() 168 { 169 if (!isset($this->stream)) { 170 throw new \RuntimeException('Stream is detached'); 171 } 172 173 return feof($this->stream); 174 } 175 176 public function tell() 177 { 178 if (!isset($this->stream)) { 179 throw new \RuntimeException('Stream is detached'); 180 } 181 182 $result = ftell($this->stream); 183 184 if ($result === false) { 185 throw new \RuntimeException('Unable to determine stream position'); 186 } 187 188 return $result; 189 } 190 191 public function rewind() 192 { 193 $this->seek(0); 194 } 195 196 public function seek($offset, $whence = SEEK_SET) 197 { 198 $whence = (int) $whence; 199 200 if (!isset($this->stream)) { 201 throw new \RuntimeException('Stream is detached'); 202 } 203 if (!$this->seekable) { 204 throw new \RuntimeException('Stream is not seekable'); 205 } 206 if (fseek($this->stream, $offset, $whence) === -1) { 207 throw new \RuntimeException('Unable to seek to stream position ' 208 . $offset . ' with whence ' . var_export($whence, true)); 209 } 210 } 211 212 public function read($length) 213 { 214 if (!isset($this->stream)) { 215 throw new \RuntimeException('Stream is detached'); 216 } 217 if (!$this->readable) { 218 throw new \RuntimeException('Cannot read from non-readable stream'); 219 } 220 if ($length < 0) { 221 throw new \RuntimeException('Length parameter cannot be negative'); 222 } 223 224 if (0 === $length) { 225 return ''; 226 } 227 228 $string = fread($this->stream, $length); 229 if (false === $string) { 230 throw new \RuntimeException('Unable to read from stream'); 231 } 232 233 return $string; 234 } 235 236 public function write($string) 237 { 238 if (!isset($this->stream)) { 239 throw new \RuntimeException('Stream is detached'); 240 } 241 if (!$this->writable) { 242 throw new \RuntimeException('Cannot write to a non-writable stream'); 243 } 244 245 // We can't know the size after writing anything 246 $this->size = null; 247 $result = fwrite($this->stream, $string); 248 249 if ($result === false) { 250 throw new \RuntimeException('Unable to write to stream'); 251 } 252 253 return $result; 254 } 255 256 public function getMetadata($key = null) 257 { 258 if (!isset($this->stream)) { 259 return $key ? null : []; 260 } elseif (!$key) { 261 return $this->customMetadata + stream_get_meta_data($this->stream); 262 } elseif (isset($this->customMetadata[$key])) { 263 return $this->customMetadata[$key]; 264 } 265 266 $meta = stream_get_meta_data($this->stream); 267 268 return isset($meta[$key]) ? $meta[$key] : null; 269 } 270} 271