Lines
76.19%
112 / 147
Methods
57.14%
12 / 21
Classes
0.00%
0 / 1
| Name | Lines | Methods | CRAP | ||||
|---|---|---|---|---|---|---|---|
| __construct | 100.00% | 3 / 3 | 100.00% | 1 / 1 | 1 | ||
| fromString | 100.00% | 7 / 7 | 100.00% | 1 / 1 | 1 | ||
| fromResource | 100.00% | 10 / 10 | 100.00% | 1 / 1 | 2 | ||
| isReadableMode | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 2 | ||
| isWritableMode | 100.00% | 5 / 5 | 100.00% | 1 / 1 | 5 | ||
| __toString | 71.42% | 5 / 7 | 0.00% | 0 / 1 | 4.37 | ||
| close | 100.00% | 11 / 11 | 100.00% | 1 / 1 | 3 | ||
| __destruct | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| detach | 100.00% | 9 / 9 | 100.00% | 1 / 1 | 1 | ||
| getSize | 45.45% | 5 / 11 | 0.00% | 0 / 1 | 9.06 | ||
| tell | 37.50% | 3 / 8 | 0.00% | 0 / 1 | 7.91 | ||
| eof | 80.00% | 4 / 5 | 0.00% | 0 / 1 | 3.07 | ||
| isSeekable | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| seek | 65.00% | 13 / 20 | 0.00% | 0 / 1 | 18.17 | ||
| rewind | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| isWritable | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| write | 72.72% | 8 / 11 | 0.00% | 0 / 1 | 5.51 | ||
| isReadable | 100.00% | 1 / 1 | 100.00% | 1 / 1 | 1 | ||
| read | 71.42% | 10 / 14 | 0.00% | 0 / 1 | 6.84 | ||
| getContents | 75.00% | 9 / 12 | 0.00% | 0 / 1 | 5.39 | ||
| getMetadata | 50.00% | 4 / 8 | 0.00% | 0 / 1 | 6.00 | ||
| 16 | final class Stream implements StreamInterface | |
| 17 | { | |
| 18 | /** @var resource|null Underlying PHP stream resource */ | |
| 19 | private $stream = null; | |
| 20 | ||
| 21 | /** @var string|null In-memory content for string-backed streams */ | |
| 22 | private ?string $content = null; | |
| 23 | ||
| 24 | /** @var int Position in in-memory content */ | |
| 25 | private int $position = 0; | |
| 26 | ||
| 27 | /** @var bool Whether the stream is readable */ | |
| 28 | private bool $readable; | |
| 29 | ||
| 30 | /** @var bool Whether the stream is writable */ | |
| 31 | private bool $writable; | |
| 32 | ||
| 33 | /** @var bool Whether the stream is seekable */ | |
| 34 | private bool $seekable; | |
| 35 | ||
| 36 | /** @var int|null Cached size */ | |
| 37 | private ?int $size = null; | |
| 38 | ||
| 39 | /** | |
| 40 | * Private constructor — use fromString() or fromResource() instead. | |
| 41 | */ | |
| 42 | private function __construct() | |
| 43 | { | |
| 44 | $this->readable = false; | |
| 45 | $this->writable = false; | |
| 46 | $this->seekable = false; | |
| 47 | } | |
| 48 | ||
| 49 | /** | |
| 50 | * Create a stream from a string. | |
| 51 | * | |
| 52 | * @param string $content The string content to wrap | |
| 53 | * @return self | |
| 54 | */ | |
| 55 | public static function fromString(string $content): self | |
| 56 | { | |
| 57 | $stream = new self(); | |
| 58 | $stream->content = $content; | |
| 59 | $stream->position = 0; | |
| 60 | $stream->readable = true; | |
| 61 | $stream->seekable = true; | |
| 62 | $stream->size = strlen($content); | |
| 63 | return $stream; | |
| 64 | } | |
| 65 | ||
| 66 | /** | |
| 67 | * Create a stream from a PHP resource. | |
| 68 | * | |
| 69 | * @param resource $resource A PHP stream resource | |
| 70 | * @return self | |
| 71 | * @throws \InvalidArgumentException If the given value is not a resource | |
| 72 | */ | |
| 73 | public static function fromResource($resource): self | |
| 74 | { | |
| 75 | if (!is_resource($resource)) { | |
| 76 | throw new \InvalidArgumentException('Stream resource must be a valid PHP resource'); | |
| 77 | } | |
| 78 | ||
| 79 | $stream = new self(); | |
| 80 | $stream->stream = $resource; | |
| 81 | $meta = stream_get_meta_data($resource); | |
| 82 | $mode = $meta['mode'] ?? ''; | |
| 83 | $stream->seekable = $meta['seekable'] ?? false; | |
| 84 | $stream->readable = self::isReadableMode($mode); | |
| 85 | $stream->writable = self::isWritableMode($mode); | |
| 86 | return $stream; | |
| 87 | } | |
| 88 | ||
| 89 | /** | |
| 90 | * Determine if a stream mode is readable. | |
| 91 | * | |
| 92 | * @see https://www.php.net/manual/en/function.fopen.php | |
| 93 | */ | |
| 94 | private static function isReadableMode(string $mode): bool | |
| 95 | { | |
| 96 | return str_starts_with($mode, 'r') || str_contains($mode, '+'); | |
| 97 | } | |
| 98 | ||
| 99 | /** | |
| 100 | * Determine if a stream mode is writable. | |
| 101 | * | |
| 102 | * @see https://www.php.net/manual/en/function.fopen.php | |
| 103 | */ | |
| 104 | private static function isWritableMode(string $mode): bool | |
| 105 | { | |
| 106 | return str_starts_with($mode, 'a') | |
| 107 | || str_starts_with($mode, 'w') | |
| 108 | || str_starts_with($mode, 'x') | |
| 109 | || str_starts_with($mode, 'c') | |
| 110 | || str_contains($mode, '+'); | |
| 111 | } | |
| 112 | ||
| 113 | public function __toString(): string | |
| 114 | { | |
| 115 | if ($this->stream !== null) { | |
| 116 | try { | |
| 117 | if ($this->isSeekable()) { | |
| 118 | $this->seek(0); | |
| 119 | } | |
| 120 | return $this->getContents(); | |
| 121 | } catch (\Throwable) { | |
| 122 | return ''; | |
| 123 | } | |
| 124 | } | |
| 125 | ||
| 126 | return $this->content ?? ''; | |
| 127 | } | |
| 128 | ||
| 129 | public function close(): void | |
| 130 | { | |
| 131 | if ($this->stream !== null) { | |
| 132 | if (is_resource($this->stream)) { | |
| 133 | fclose($this->stream); | |
| 134 | } | |
| 135 | $this->detach(); | |
| 136 | return; | |
| 137 | } | |
| 138 | ||
| 139 | $this->content = null; | |
| 140 | $this->position = 0; | |
| 141 | $this->size = null; | |
| 142 | $this->readable = false; | |
| 143 | $this->writable = false; | |
| 144 | $this->seekable = false; | |
| 145 | } | |
| 146 | ||
| 147 | /** | |
| 148 | * Closes the stream on destruction. | |
| 149 | * | |
| 150 | * @return void | |
| 151 | */ | |
| 152 | public function __destruct() | |
| 153 | { | |
| 154 | $this->close(); | |
| 155 | } | |
| 156 | ||
| 157 | public function detach() | |
| 158 | { | |
| 159 | $resource = $this->stream; | |
| 160 | $this->stream = null; | |
| 161 | $this->content = null; | |
| 162 | $this->position = 0; | |
| 163 | $this->size = null; | |
| 164 | $this->readable = false; | |
| 165 | $this->writable = false; | |
| 166 | $this->seekable = false; | |
| 167 | return $resource; | |
| 168 | } | |
| 169 | ||
| 170 | public function getSize(): ?int | |
| 171 | { | |
| 172 | if ($this->size !== null) { | |
| 173 | return $this->size; | |
| 174 | } | |
| 175 | ||
| 176 | if ($this->stream !== null) { | |
| 177 | $stats = fstat($this->stream); | |
| 178 | if ($stats !== false) { | |
| 179 | $this->size = $stats['size']; | |
| 180 | return $this->size; | |
| 181 | } | |
| 182 | } | |
| 183 | ||
| 184 | if ($this->content !== null) { | |
| 185 | $this->size = strlen($this->content); | |
| 186 | return $this->size; | |
| 187 | } | |
| 188 | ||
| 189 | return null; | |
| 190 | } | |
| 191 | ||
| 192 | public function tell(): int | |
| 193 | { | |
| 194 | if ($this->stream !== null) { | |
| 195 | $position = ftell($this->stream); | |
| 196 | if ($position === false) { | |
| 197 | throw new RuntimeException('Unable to determine stream position'); | |
| 198 | } | |
| 199 | return $position; | |
| 200 | } | |
| 201 | ||
| 202 | if ($this->content !== null) { | |
| 203 | return $this->position; | |
| 204 | } | |
| 205 | ||
| 206 | throw new RuntimeException('Stream is closed or unusable'); | |
| 207 | } | |
| 208 | ||
| 209 | public function eof(): bool | |
| 210 | { | |
| 211 | if ($this->stream !== null) { | |
| 212 | return feof($this->stream); | |
| 213 | } | |
| 214 | ||
| 215 | if ($this->content !== null) { | |
| 216 | return $this->position >= strlen($this->content); | |
| 217 | } | |
| 218 | ||
| 219 | return true; | |
| 220 | } | |
| 221 | ||
| 222 | public function isSeekable(): bool | |
| 223 | { | |
| 224 | return $this->seekable; | |
| 225 | } | |
| 226 | ||
| 227 | public function seek(int $offset, int $whence = SEEK_SET): void | |
| 228 | { | |
| 229 | if ($this->stream !== null) { | |
| 230 | if (! $this->seekable) { | |
| 231 | throw new RuntimeException('Stream is not seekable'); | |
| 232 | } | |
| 233 | if (fseek($this->stream, $offset, $whence) !== 0) { | |
| 234 | throw new RuntimeException('Unable to seek to position'); | |
| 235 | } | |
| 236 | return; | |
| 237 | } | |
| 238 | ||
| 239 | if ($this->content !== null) { | |
| 240 | if (! $this->seekable) { | |
| 241 | throw new RuntimeException('Stream is not seekable'); | |
| 242 | } | |
| 243 | $length = strlen($this->content); | |
| 244 | $newPosition = match ($whence) { | |
| 245 | SEEK_SET => $offset, | |
| 246 | SEEK_CUR => $this->position + $offset, | |
| 247 | SEEK_END => $length + $offset, | |
| 248 | default => throw new RuntimeException("Invalid whence value: $whence"), | |
| 249 | }; | |
| 250 | // An out-of-range position is a seek failure — throw rather | |
| 251 | // than silently clamp. | |
| 252 | if ($newPosition < 0 || $newPosition > $length) { | |
| 253 | throw new RuntimeException("Unable to seek to position $newPosition (stream length: $length)"); | |
| 254 | } | |
| 255 | $this->position = $newPosition; | |
| 256 | return; | |
| 257 | } | |
| 258 | ||
| 259 | throw new RuntimeException('Stream is closed or unusable'); | |
| 260 | } | |
| 261 | ||
| 262 | public function rewind(): void | |
| 263 | { | |
| 264 | $this->seek(0); | |
| 265 | } | |
| 266 | ||
| 267 | public function isWritable(): bool | |
| 268 | { | |
| 269 | return $this->writable; | |
| 270 | } | |
| 271 | ||
| 272 | public function write(string $string): int | |
| 273 | { | |
| 274 | if ($this->stream !== null) { | |
| 275 | if (! $this->writable) { | |
| 276 | throw new RuntimeException('Stream is not writable'); | |
| 277 | } | |
| 278 | $bytes = fwrite($this->stream, $string); | |
| 279 | if ($bytes === false) { | |
| 280 | throw new RuntimeException('Unable to write to stream'); | |
| 281 | } | |
| 282 | $this->size = null; | |
| 283 | return $bytes; | |
| 284 | } | |
| 285 | ||
| 286 | if ($this->content !== null) { | |
| 287 | throw new RuntimeException('String-backed streams are not writable'); | |
| 288 | } | |
| 289 | ||
| 290 | throw new RuntimeException('Stream is closed or unusable'); | |
| 291 | } | |
| 292 | ||
| 293 | public function isReadable(): bool | |
| 294 | { | |
| 295 | return $this->readable; | |
| 296 | } | |
| 297 | ||
| 298 | public function read(int $length): string | |
| 299 | { | |
| 300 | if ($this->stream !== null) { | |
| 301 | if (! $this->readable) { | |
| 302 | throw new RuntimeException('Stream is not readable'); | |
| 303 | } | |
| 304 | $data = fread($this->stream, $length); | |
| 305 | if ($data === false) { | |
| 306 | throw new RuntimeException('Unable to read from stream'); | |
| 307 | } | |
| 308 | return $data; | |
| 309 | } | |
| 310 | ||
| 311 | if ($this->content !== null) { | |
| 312 | if (! $this->readable) { | |
| 313 | throw new RuntimeException('Stream is not readable'); | |
| 314 | } | |
| 315 | $data = substr($this->content, $this->position, $length); | |
| 316 | $this->position += strlen($data); | |
| 317 | return $data; | |
| 318 | } | |
| 319 | ||
| 320 | throw new RuntimeException('Stream is closed or unusable'); | |
| 321 | } | |
| 322 | ||
| 323 | public function getContents(): string | |
| 324 | { | |
| 325 | if ($this->stream !== null) { | |
| 326 | if (! $this->readable) { | |
| 327 | throw new RuntimeException('Stream is not readable'); | |
| 328 | } | |
| 329 | $contents = stream_get_contents($this->stream); | |
| 330 | if ($contents === false) { | |
| 331 | throw new RuntimeException('Unable to read stream contents'); | |
| 332 | } | |
| 333 | return $contents; | |
| 334 | } | |
| 335 | ||
| 336 | if ($this->content !== null) { | |
| 337 | $data = substr($this->content, $this->position); | |
| 338 | $this->position = strlen($this->content); | |
| 339 | return $data; | |
| 340 | } | |
| 341 | ||
| 342 | throw new RuntimeException('Stream is closed or unusable'); | |
| 343 | } | |
| 344 | ||
| 345 | public function getMetadata(?string $key = null) | |
| 346 | { | |
| 347 | if ($this->stream !== null) { | |
| 348 | $meta = stream_get_meta_data($this->stream); | |
| 349 | if ($key === null) { | |
| 350 | return $meta; | |
| 351 | } | |
| 352 | return $meta[$key] ?? null; | |
| 353 | } | |
| 354 | ||
| 355 | if ($key === null) { | |
| 356 | return []; | |
| 357 | } | |
| 358 | ||
| 359 | return null; | |
| 360 | } | |
| 361 | } |