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