<?php
declare(strict_types=1);
namespace SHServ\Integrations\GAuth\Http;
use Psr\Http\Message\StreamInterface;
final class Psr7Stream implements StreamInterface
{
private string $content;
private int $position = 0;
private bool $readable;
private bool $writable;
private bool $seekable;
/** @var resource|null */
private $resource = null;
public function __construct(string $content = '', bool $readable = true, bool $writable = true)
{
$this->content = $content;
$this->readable = $readable;
$this->writable = $writable;
$this->seekable = true;
$this->resource = fopen('php://temp', 'r+');
if ($this->resource !== false && $content !== '') {
fwrite($this->resource, $content);
rewind($this->resource);
}
}
public static function fromResource($resource): self
{
$content = stream_get_contents($resource) ?: '';
$instance = new self($content);
$instance->resource = $resource;
$instance->seekable = (bool) stream_get_meta_data($resource)['seekable'];
$meta = stream_get_meta_data($resource);
$mode = $meta['mode'] ?? '';
$instance->readable = strpbrk($mode, 'r+') !== false;
$instance->writable = strpbrk($mode, 'waxc+') !== false;
return $instance;
}
public function __toString(): string
{
try {
$this->rewind();
return $this->getContents();
} catch (\Throwable $e) {
return '';
}
}
public function close(): void
{
if ($this->resource !== null) {
fclose($this->resource);
$this->resource = null;
}
}
public function detach()
{
$res = $this->resource;
$this->resource = null;
return $res;
}
public function getSize(): ?int
{
if ($this->resource === null) {
return null;
}
$stat = fstat($this->resource);
return $stat ? $stat['size'] : null;
}
public function tell(): int
{
if ($this->resource === null) {
throw new \RuntimeException('Stream is detached');
}
$pos = ftell($this->resource);
if ($pos === false) {
throw new \RuntimeException('Unable to determine stream position');
}
return $pos;
}
public function eof(): bool
{
if ($this->resource === null) {
return true;
}
return feof($this->resource);
}
public function isSeekable(): bool
{
return $this->seekable;
}
public function seek(int $offset, int $whence = \SEEK_SET): void
{
if (!$this->seekable || $this->resource === null) {
throw new \RuntimeException('Stream is not seekable');
}
if (fseek($this->resource, $offset, $whence) === -1) {
throw new \RuntimeException('Unable to seek stream');
}
}
public function rewind(): void
{
$this->seek(0);
}
public function isWritable(): bool
{
return $this->writable && $this->resource !== null;
}
public function write(string $string): int
{
if (!$this->isWritable()) {
throw new \RuntimeException('Stream is not writable');
}
$written = fwrite($this->resource, $string);
if ($written === false) {
throw new \RuntimeException('Unable to write to stream');
}
return $written;
}
public function isReadable(): bool
{
return $this->readable && $this->resource !== null;
}
public function read(int $length): string
{
if (!$this->isReadable()) {
throw new \RuntimeException('Stream is not readable');
}
$data = fread($this->resource, $length);
if ($data === false) {
throw new \RuntimeException('Unable to read from stream');
}
return $data;
}
public function getContents(): string
{
if (!$this->isReadable()) {
throw new \RuntimeException('Stream is not readable');
}
$contents = stream_get_contents($this->resource);
if ($contents === false) {
throw new \RuntimeException('Unable to read stream contents');
}
return $contents;
}
public function getMetadata(?string $key = null): mixed
{
if ($this->resource === null) {
return $key === null ? [] : null;
}
$meta = stream_get_meta_data($this->resource);
if ($key === null) {
return $meta;
}
return $meta[$key] ?? null;
}
}