1<?php 2 3declare(strict_types=1); 4 5namespace GuzzleHttp\Psr7; 6 7use Psr\Http\Message\StreamInterface; 8 9/** 10 * PHP stream implementation. 11 */ 12class Stream implements StreamInterface 13{ 14 /** 15 * @see https://www.php.net/manual/en/function.fopen.php 16 * @see https://www.php.net/manual/en/function.gzopen.php 17 */ 18 private const READABLE_MODES = '/r|a\+|ab\+|w\+|wb\+|x\+|xb\+|c\+|cb\+/'; 19 private const WRITABLE_MODES = '/a|w|r\+|rb\+|rw|x|c/'; 20 21 /** @var resource */ 22 private $stream; 23 /** @var int|null */ 24 private $size; 25 /** @var bool */ 26 private $seekable; 27 /** @var bool */ 28 private $readable; 29 /** @var bool */ 30 private $writable; 31 /** @var string|null */ 32 private $uri; 33 /** @var mixed[] */ 34 private $customMetadata; 35 36 /** 37 * This constructor accepts an associative array of options. 38 * 39 * - size: (int) If a read stream would otherwise have an indeterminate 40 * size, but the size is known due to foreknowledge, then you can 41 * provide that size, in bytes. 42 * - metadata: (array) Any additional metadata to return when the metadata 43 * of the stream is accessed. 44 * 45 * @param resource $stream Stream resource to wrap. 46 * @param array{size?: int, metadata?: array} $options Associative array of options. 47 * 48 * @throws \InvalidArgumentException if the stream is not a stream resource 49 */ 50 public function __construct($stream, array $options = []) 51 { 52 if (!is_resource($stream)) { 53 throw new \InvalidArgumentException('Stream must be a resource'); 54 } 55 56 if (isset($options['size'])) { 57 $this->size = $options['size']; 58 } 59 60 $this->customMetadata = $options['metadata'] ?? []; 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(): string 78 { 79 try { 80 if ($this->isSeekable()) { 81 $this->seek(0); 82 } 83 84 return $this->getContents(); 85 } catch (\Throwable $e) { 86 if (\PHP_VERSION_ID >= 70400) { 87 throw $e; 88 } 89 trigger_error(sprintf('%s::__toString exception: %s', self::class, (string) $e), E_USER_ERROR); 90 91 return ''; 92 } 93 } 94 95 public function getContents(): string 96 { 97 if (!isset($this->stream)) { 98 throw new \RuntimeException('Stream is detached'); 99 } 100 101 if (!$this->readable) { 102 throw new \RuntimeException('Cannot read from non-readable stream'); 103 } 104 105 return Utils::tryGetContents($this->stream); 106 } 107 108 public function close(): void 109 { 110 if (isset($this->stream)) { 111 if (is_resource($this->stream)) { 112 fclose($this->stream); 113 } 114 $this->detach(); 115 } 116 } 117 118 public function detach() 119 { 120 if (!isset($this->stream)) { 121 return null; 122 } 123 124 $result = $this->stream; 125 unset($this->stream); 126 $this->size = $this->uri = null; 127 $this->readable = $this->writable = $this->seekable = false; 128 129 return $result; 130 } 131 132 public function getSize(): ?int 133 { 134 if ($this->size !== null) { 135 return $this->size; 136 } 137 138 if (!isset($this->stream)) { 139 return null; 140 } 141 142 // Clear the stat cache if the stream has a URI 143 if ($this->uri) { 144 clearstatcache(true, $this->uri); 145 } 146 147 $stats = fstat($this->stream); 148 if (is_array($stats) && isset($stats['size'])) { 149 $this->size = $stats['size']; 150 151 return $this->size; 152 } 153 154 return null; 155 } 156 157 public function isReadable(): bool 158 { 159 return $this->readable; 160 } 161 162 public function isWritable(): bool 163 { 164 return $this->writable; 165 } 166 167 public function isSeekable(): bool 168 { 169 return $this->seekable; 170 } 171 172 public function eof(): bool 173 { 174 if (!isset($this->stream)) { 175 throw new \RuntimeException('Stream is detached'); 176 } 177 178 return feof($this->stream); 179 } 180 181 public function tell(): int 182 { 183 if (!isset($this->stream)) { 184 throw new \RuntimeException('Stream is detached'); 185 } 186 187 $result = ftell($this->stream); 188 189 if ($result === false) { 190 throw new \RuntimeException('Unable to determine stream position'); 191 } 192 193 return $result; 194 } 195 196 public function rewind(): void 197 { 198 $this->seek(0); 199 } 200 201 public function seek($offset, $whence = SEEK_SET): void 202 { 203 $whence = (int) $whence; 204 205 if (!isset($this->stream)) { 206 throw new \RuntimeException('Stream is detached'); 207 } 208 if (!$this->seekable) { 209 throw new \RuntimeException('Stream is not seekable'); 210 } 211 if (fseek($this->stream, $offset, $whence) === -1) { 212 throw new \RuntimeException('Unable to seek to stream position ' 213 .$offset.' with whence '.var_export($whence, true)); 214 } 215 } 216 217 public function read($length): string 218 { 219 if (!isset($this->stream)) { 220 throw new \RuntimeException('Stream is detached'); 221 } 222 if (!$this->readable) { 223 throw new \RuntimeException('Cannot read from non-readable stream'); 224 } 225 if ($length < 0) { 226 throw new \RuntimeException('Length parameter cannot be negative'); 227 } 228 229 if (0 === $length) { 230 return ''; 231 } 232 233 try { 234 $string = fread($this->stream, $length); 235 } catch (\Exception $e) { 236 throw new \RuntimeException('Unable to read from stream', 0, $e); 237 } 238 239 if (false === $string) { 240 throw new \RuntimeException('Unable to read from stream'); 241 } 242 243 return $string; 244 } 245 246 public function write($string): int 247 { 248 if (!isset($this->stream)) { 249 throw new \RuntimeException('Stream is detached'); 250 } 251 if (!$this->writable) { 252 throw new \RuntimeException('Cannot write to a non-writable stream'); 253 } 254 255 // We can't know the size after writing anything 256 $this->size = null; 257 $result = fwrite($this->stream, $string); 258 259 if ($result === false) { 260 throw new \RuntimeException('Unable to write to stream'); 261 } 262 263 return $result; 264 } 265 266 /** 267 * @return mixed 268 */ 269 public function getMetadata($key = null) 270 { 271 if (!isset($this->stream)) { 272 return $key ? null : []; 273 } elseif (!$key) { 274 return $this->customMetadata + stream_get_meta_data($this->stream); 275 } elseif (isset($this->customMetadata[$key])) { 276 return $this->customMetadata[$key]; 277 } 278 279 $meta = stream_get_meta_data($this->stream); 280 281 return $meta[$key] ?? null; 282 } 283} 284