stream = $stream; $metadata = stream_get_meta_data($this->stream); $this->seekable = $metadata['seekable']; $this->readable = (bool) preg_match(self::READABLE_MODES, $metadata['mode']); $this->writable = (bool) preg_match(self::WRITABLE_MODES, $metadata['mode']); } /** * {@inheritDoc} */ public function __toString(): string { if ($this->isSeekable()) { $this->seek(0); } return $this->getContents(); } /** * {@inheritDoc} */ public function close(): void { if (isset($this->stream)) { fclose($this->stream); } $this->detach(); } /** * {@inheritDoc} */ public function detach() { if (!isset($this->stream)) { return null; } $result = $this->stream; unset($this->stream); $this->readable = $this->writable = $this->seekable = false; return $result; } /** * {@inheritDoc} */ public function getSize(): ?int { if (!isset($this->stream)) { return null; } $stats = fstat($this->stream); if (is_array($stats) && isset($stats['size'])) { return $stats['size']; } return null; } /** * {@inheritDoc} */ public function tell(): int { if (!isset($this->stream)) { throw new RuntimeException('Stream is detached'); } $result = ftell($this->stream); if ($result === false) { throw new RuntimeException('Unable to determine stream position'); } return $result; } /** * {@inheritDoc} */ public function eof(): bool { if (!isset($this->stream)) { throw new RuntimeException('Stream is detached'); } return feof($this->stream); } /** * {@inheritDoc} */ public function isSeekable(): bool { return $this->seekable; } /** * {@inheritDoc} */ public function seek(int $offset, int $whence = SEEK_SET): void { if (!isset($this->stream)) { throw new RuntimeException('Stream is detached'); } if (!$this->isSeekable()) { throw new RuntimeException('Stream is not seekable'); } if (fseek($this->stream, $offset, $whence) === -1) { throw new RuntimeException("Unable to seek stream from offset {$offset} to whence {$whence}"); } } /** * {@inheritDoc} */ public function rewind(): void { $this->seek(0); } /** * {@inheritDoc} */ public function isWritable(): bool { return $this->writable; } /** * {@inheritDoc} * @throws RuntimeException */ public function write(string $string): int { if (!isset($this->stream)) { throw new RuntimeException('Stream is detached'); } if (!$this->isWritable()) { throw new RuntimeException('Cannot write to a non-writable stream'); } $result = fwrite($this->stream, $string); if ($result === false || $result === 0) { $metadata = $this->getMetadata(); if ($this->eof()) { throw new RuntimeException('Connection closed by peer during write', 1); } if (!is_resource($this->stream)) { throw new RuntimeException( 'Stream resource is no longer valid', 1 ); } if (array_key_exists('timed_out', $metadata) && $metadata['timed_out']) { throw new RuntimeException( 'Stream has been timed out', 2 ); } throw new RuntimeException('Unable to write to stream', 1); } return $result; } /** * {@inheritDoc} */ public function isReadable(): bool { return $this->readable; } /** * {@inheritDoc} * @param int $length If length = -1, reads a stream line by line (e.g fgets()) * @throws RuntimeException */ public function read(int $length): string { if (!isset($this->stream)) { throw new RuntimeException('Stream is detached'); } if (!$this->isReadable()) { throw new RuntimeException('Cannot read from non-readable stream'); } if ($length < -1) { throw new RuntimeException('Length parameter cannot be negative'); } if (0 === $length) { return ''; } if ($length === -1) { $string = fgets($this->stream); } else { $string = fread($this->stream, $length); } if (false === $string) { $metadata = $this->getMetadata(); if ($this->eof()) { throw new RuntimeException('Connection closed by peer during read', 1); } if (!is_resource($this->stream)) { throw new RuntimeException( 'Stream resource is no longer valid', 1 ); } if (array_key_exists('timed_out', $metadata) && $metadata['timed_out']) { throw new RuntimeException( 'Stream has been timed out', 2 ); } throw new RuntimeException('Unable to read from stream', 1); } return $string; } /** * {@inheritDoc} */ public function getContents(): string { if (!isset($this->stream)) { throw new RuntimeException('Stream is detached'); } if (!$this->isReadable()) { throw new RuntimeException('Cannot read from non-readable stream'); } return stream_get_contents($this->stream); } /** * {@inheritDoc} * @return mixed */ public function getMetadata(?string $key = null) { if (!isset($this->stream)) { return null; } if (!$key) { return stream_get_meta_data($this->stream); } $metadata = stream_get_meta_data($this->stream); return $metadata[$key] ?? null; } }