mirror of
https://github.com/shufflingpixels/php-io.git
synced 2026-10-19 14:36:46 +02:00
stream: adopt PSR-7 StreamInterface
This commit is contained in:
parent
8d8690f77b
commit
89525294de
12 changed files with 250 additions and 152 deletions
|
|
@ -3,6 +3,7 @@
|
|||
namespace Shufflingpixels\IO;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class BinaryReader
|
||||
|
|
@ -23,7 +24,12 @@ class BinaryReader
|
|||
|
||||
public function length() : int
|
||||
{
|
||||
return $this->stream->length();
|
||||
$size = $this->stream->getSize();
|
||||
if ($size === null) {
|
||||
throw new RuntimeException('Stream size is not known');
|
||||
}
|
||||
|
||||
return $size;
|
||||
}
|
||||
|
||||
public function tell() : int
|
||||
|
|
@ -36,9 +42,9 @@ class BinaryReader
|
|||
return $this->stream->eof();
|
||||
}
|
||||
|
||||
public function seek(int $position, SeekMode $mode = SeekMode::SET)
|
||||
public function seek(int $position, int $whence = SEEK_SET): void
|
||||
{
|
||||
$this->stream->seek($position, $mode);
|
||||
$this->stream->seek($position, $whence);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
117
src/Buffer.php
117
src/Buffer.php
|
|
@ -4,11 +4,13 @@ namespace Shufflingpixels\IO;
|
|||
|
||||
use InvalidArgumentException;
|
||||
use OutOfBoundsException;
|
||||
use Shufflingpixels\IO\Exception\EndOfStreamException;
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use RuntimeException;
|
||||
|
||||
class Buffer implements StreamInterface
|
||||
{
|
||||
private int $position = 0;
|
||||
private bool $detached = false;
|
||||
|
||||
public function __construct(protected string $data)
|
||||
{
|
||||
|
|
@ -16,6 +18,25 @@ class Buffer implements StreamInterface
|
|||
|
||||
public function close(): void
|
||||
{
|
||||
$this->detach();
|
||||
}
|
||||
|
||||
public function detach(): mixed
|
||||
{
|
||||
if ($this->detached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->detached = true;
|
||||
$this->data = '';
|
||||
$this->position = 0;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getSize(): ?int
|
||||
{
|
||||
return $this->detached ? null : $this->length();
|
||||
}
|
||||
|
||||
public function length(): int
|
||||
|
|
@ -25,6 +46,10 @@ class Buffer implements StreamInterface
|
|||
|
||||
public function remaining() : int
|
||||
{
|
||||
if ($this->detached) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->length() - $this->position;
|
||||
}
|
||||
|
||||
|
|
@ -35,6 +60,8 @@ class Buffer implements StreamInterface
|
|||
|
||||
public function tell() : int
|
||||
{
|
||||
$this->ensureAttached();
|
||||
|
||||
return $this->position;
|
||||
}
|
||||
|
||||
|
|
@ -43,22 +70,29 @@ class Buffer implements StreamInterface
|
|||
return true;
|
||||
}
|
||||
|
||||
public function seek(int $offset, SeekMode $mode = SeekMode::SET): void
|
||||
public function seek(int $offset, int $whence = SEEK_SET): void
|
||||
{
|
||||
$position = match($mode) {
|
||||
SeekMode::SET => $offset,
|
||||
SeekMode::CUR => $this->position + $offset,
|
||||
SeekMode::END => $this->length() + $offset,
|
||||
$this->ensureAttached();
|
||||
|
||||
$position = match($whence) {
|
||||
SEEK_SET => $offset,
|
||||
SEEK_CUR => $this->position + $offset,
|
||||
SEEK_END => $this->length() + $offset,
|
||||
default => throw new InvalidArgumentException("Invalid seek mode")
|
||||
};
|
||||
|
||||
if ($position > $this->length()) {
|
||||
if ($position < 0 || $position > $this->length()) {
|
||||
throw new OutOfBoundsException("Seek position out of bounds: {$position}");
|
||||
}
|
||||
|
||||
$this->position = $position;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function isReadable(): bool
|
||||
{
|
||||
return true;
|
||||
|
|
@ -66,30 +100,37 @@ class Buffer implements StreamInterface
|
|||
|
||||
public function read(int $length): string
|
||||
{
|
||||
$this->ensureAttached();
|
||||
|
||||
if ($length < 0) {
|
||||
throw new InvalidArgumentException("Length must be >= 0");
|
||||
}
|
||||
|
||||
if ($this->remaining() < $length) {
|
||||
throw new EndOfStreamException(
|
||||
"Not enough bytes to read {$length} byte(s), {$this->remaining()} remaining"
|
||||
);
|
||||
if ($length === 0 || $this->eof()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$result = substr($this->data, $this->position, $length);
|
||||
$this->position += $length;
|
||||
$result = substr($this->data, $this->position, min($length, $this->remaining()));
|
||||
$this->position += strlen($result);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function isWriteable(): bool
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function write(string $data): int
|
||||
public function isWriteable(): bool
|
||||
{
|
||||
$length = \strlen($data);
|
||||
return $this->isWritable();
|
||||
}
|
||||
|
||||
public function write(string $string): int
|
||||
{
|
||||
$this->ensureAttached();
|
||||
|
||||
$length = \strlen($string);
|
||||
|
||||
if ($length === 0) {
|
||||
return 0;
|
||||
|
|
@ -101,9 +142,51 @@ class Buffer implements StreamInterface
|
|||
? \substr($this->data, $suffixStart)
|
||||
: '';
|
||||
|
||||
$this->data = $prefix . $data . $suffix;
|
||||
$this->data = $prefix . $string . $suffix;
|
||||
$this->position += $length;
|
||||
|
||||
return $length;
|
||||
}
|
||||
|
||||
public function getContents(): string
|
||||
{
|
||||
$this->ensureAttached();
|
||||
|
||||
if ($this->eof()) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$result = substr($this->data, $this->position);
|
||||
$this->position = $this->length();
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function getMetadata(?string $key = null): mixed
|
||||
{
|
||||
$metadata = [
|
||||
'seekable' => true,
|
||||
'readable' => true,
|
||||
'writable' => true,
|
||||
'uri' => null,
|
||||
];
|
||||
|
||||
return $key !== null ? $metadata[$key] ?? null : $metadata;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
if ($this->detached) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
private function ensureAttached(): void
|
||||
{
|
||||
if ($this->detached) {
|
||||
throw new RuntimeException('Stream is detached');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
use Psr\Http\Message\StreamInterface;
|
||||
use Shufflingpixels\IO\Exception\IOException;
|
||||
|
||||
abstract class Resource implements StreamInterface
|
||||
|
|
@ -21,13 +22,24 @@ abstract class Resource implements StreamInterface
|
|||
|
||||
public function close(): void
|
||||
{
|
||||
fclose($this->resource);
|
||||
if ($this->resource !== null) {
|
||||
fclose($this->resource);
|
||||
$this->resource = null;
|
||||
}
|
||||
}
|
||||
|
||||
public function length() : int
|
||||
public function detach(): mixed
|
||||
{
|
||||
$resource = $this->resource;
|
||||
$this->resource = null;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public function getSize(): ?int
|
||||
{
|
||||
if (!$this->isSeekable()) {
|
||||
throw new IOException("Unable to get length on a non-seekable stream");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->size === null) {
|
||||
|
|
@ -56,13 +68,13 @@ abstract class Resource implements StreamInterface
|
|||
return ftell($this->resource);
|
||||
}
|
||||
|
||||
public function seek(int $position, SeekMode $mode = SeekMode::SET): void
|
||||
public function seek(int $position, int $whence = SEEK_SET): void
|
||||
{
|
||||
if (!$this->isSeekable()) {
|
||||
throw new IOException("Unable to seek on a non-seekable stream");
|
||||
}
|
||||
|
||||
if (fseek($this->resource, $position, $mode->value) < 0) {
|
||||
if (fseek($this->resource, $position, $whence) < 0) {
|
||||
throw new IOException("Unable to seek to the given position");
|
||||
}
|
||||
}
|
||||
|
|
@ -77,18 +89,23 @@ abstract class Resource implements StreamInterface
|
|||
return fread($this->resource, $length);
|
||||
}
|
||||
|
||||
public function isWriteable(): bool
|
||||
public function isWritable(): bool
|
||||
{
|
||||
return $this->writeable;
|
||||
}
|
||||
|
||||
public function write(string $data): int
|
||||
public function isWriteable(): bool
|
||||
{
|
||||
return $this->isWritable();
|
||||
}
|
||||
|
||||
public function write(string $string): int
|
||||
{
|
||||
if (!$this->writeable) {
|
||||
throw new IOException("Stream is not writeable");
|
||||
}
|
||||
|
||||
$result = fwrite($this->resource, $data);
|
||||
$result = fwrite($this->resource, $string);
|
||||
if ($result === false) {
|
||||
throw new IOException("Failed to write to stream");
|
||||
}
|
||||
|
|
@ -96,4 +113,26 @@ abstract class Resource implements StreamInterface
|
|||
$this->size = null;
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function rewind(): void
|
||||
{
|
||||
$this->seek(0);
|
||||
}
|
||||
|
||||
public function getContents(): string
|
||||
{
|
||||
return (string) stream_get_contents($this->resource);
|
||||
}
|
||||
|
||||
public function getMetadata(?string $key = null): mixed
|
||||
{
|
||||
$data = stream_get_meta_data($this->resource);
|
||||
|
||||
return $key !== null ? $data[$key] ?? null : $data;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->getContents();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
/**
|
||||
* Basic wrapper around SEEK_* constants for type-safety.
|
||||
*/
|
||||
enum SeekMode : int
|
||||
{
|
||||
// Set to start of stream.
|
||||
case SET = SEEK_SET;
|
||||
|
||||
// Set to current position in stream.
|
||||
case CUR = SEEK_CUR;
|
||||
|
||||
// Set to end of file.
|
||||
case END = SEEK_END;
|
||||
}
|
||||
|
|
@ -1,69 +0,0 @@
|
|||
<?php
|
||||
|
||||
namespace Shufflingpixels\IO;
|
||||
|
||||
interface StreamInterface
|
||||
{
|
||||
/**
|
||||
* Close the underlying resource.
|
||||
*/
|
||||
public function close() : void;
|
||||
|
||||
/**
|
||||
* returns true if stream is at End-of-file
|
||||
*/
|
||||
public function eof() : bool;
|
||||
|
||||
/**
|
||||
* Get the length of the stream,
|
||||
* if length can't be returned RuntimeException is thrown.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function length() : int;
|
||||
|
||||
/**
|
||||
* Returns true if the stream supports seeking.
|
||||
*/
|
||||
public function isSeekable() : bool;
|
||||
|
||||
/**
|
||||
* Seek to a position in the stream.
|
||||
* may throw RuntimeException if seeking is not supported.
|
||||
*
|
||||
* @throws \OutOfBoundsException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function seek(int $position, SeekMode $mode = SeekMode::SET): void;
|
||||
|
||||
/**
|
||||
* Get the current position in the stream.
|
||||
*/
|
||||
public function tell() : int;
|
||||
|
||||
/**
|
||||
* Returns true if stream can be read from.
|
||||
*/
|
||||
public function isReadable() : bool;
|
||||
|
||||
/**
|
||||
* Read from stream, may throw RuntimeException if stream is not readable.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function read(int $length): string;
|
||||
|
||||
/**
|
||||
* Returns true if stream can be written to.
|
||||
*/
|
||||
public function isWriteable() : bool;
|
||||
|
||||
/**
|
||||
* Write to stream.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function write(string $data) : int;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue