Newer
Older
smart-home-server / server / SHServ / Integrations / GAuth / Http / Psr7Stream.php
<?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;
    }
}