1
0
Fork 0
mirror of https://github.com/shufflingpixels/php-io.git synced 2026-08-15 11:48:13 +02:00

Add documentation to classes and methods

This commit is contained in:
Henrik Hautakoski 2026-06-26 13:40:50 +02:00
parent 4bf5d8084e
commit eadc96599b
9 changed files with 362 additions and 8 deletions

View file

@ -9,12 +9,23 @@ use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* Reads primitive binary types from a PSR-7 stream.
*
* Integer methods follow the naming convention read{Signedness}{Bits}{Endianness},
* e.g. readInt16LE for a signed 16-bit little-endian integer.
*/
class BinaryReader
{
public function __construct(protected StreamInterface $stream)
{
}
/**
* Returns the total byte length of the stream.
*
* @throws \RuntimeException if the stream size is not known
*/
public function length() : int
{
$size = $this->stream->getSize();
@ -25,24 +36,37 @@ class BinaryReader
return $size;
}
/**
* Returns the current byte offset of the stream cursor.
*/
public function tell() : int
{
return $this->stream->tell();
}
/**
* Returns true when the cursor is at the end of the stream.
*/
public function eof() : bool
{
return $this->stream->eof();
}
/**
* Moves the stream cursor to the given position.
*
* @param int $whence SEEK_SET, SEEK_CUR, or SEEK_END
*/
public function seek(int $position, int $whence = SEEK_SET): void
{
$this->stream->seek($position, $whence);
}
/**
* @throws \InvalidArgumentException
* @throws \RuntimeException
* Reads exactly $length bytes, throwing if fewer are available.
*
* @throws \InvalidArgumentException if $length is negative
* @throws \RuntimeException if the stream returns fewer bytes than requested
*/
public function read(int $length): string
{
@ -60,11 +84,17 @@ class BinaryReader
return $data;
}
/**
* Reads an unsigned 8-bit integer (0255).
*/
public function readUInt8(): int
{
return \ord($this->read(1));
}
/**
* Reads a signed 8-bit integer (-128127).
*/
public function readInt8(): int
{
$value = $this->readUInt8();
@ -72,16 +102,25 @@ class BinaryReader
return $value >= 0x80 ? $value - 0x100 : $value;
}
/**
* Reads an unsigned 16-bit little-endian integer.
*/
public function readUInt16LE(): int
{
return \unpack('v', $this->read(2))[1];
}
/**
* Reads an unsigned 16-bit big-endian integer.
*/
public function readUInt16BE(): int
{
return \unpack('n', $this->read(2))[1];
}
/**
* Reads a signed 16-bit little-endian integer.
*/
public function readInt16LE(): int
{
$value = $this->readUInt16LE();
@ -89,6 +128,9 @@ class BinaryReader
return $value >= 0x8000 ? $value - 0x1_0000 : $value;
}
/**
* Reads a signed 16-bit big-endian integer.
*/
public function readInt16BE(): int
{
$value = $this->readUInt16BE();
@ -96,16 +138,25 @@ class BinaryReader
return $value >= 0x8000 ? $value - 0x1_0000 : $value;
}
/**
* Reads an unsigned 32-bit little-endian integer.
*/
public function readUInt32LE(): int
{
return \unpack('V', $this->read(4))[1];
}
/**
* Reads an unsigned 32-bit big-endian integer.
*/
public function readUInt32BE(): int
{
return \unpack('N', $this->read(4))[1];
}
/**
* Reads a signed 32-bit little-endian integer.
*/
public function readInt32LE(): int
{
$value = $this->readUInt32LE();
@ -113,6 +164,9 @@ class BinaryReader
return $value >= 0x8000_0000 ? $value - 0x1_0000_0000 : $value;
}
/**
* Reads a signed 32-bit big-endian integer.
*/
public function readInt32BE(): int
{
$value = $this->readUInt32BE();
@ -121,7 +175,9 @@ class BinaryReader
}
/**
* Reads a string of fixed length, trimming any padding characters from the end.
* Reads exactly $length bytes and strips trailing $pad_chars from the result.
*
* The cursor always advances by $length bytes regardless of how much padding is trimmed.
*/
public function readPaddedString(int $length, string $pad_chars = "\x00") : string
{

View file

@ -8,79 +8,133 @@ namespace Shufflingpixels\IO;
use InvalidArgumentException;
use Psr\Http\Message\StreamInterface;
/**
* Writes primitive binary types to a PSR-7 stream.
*
* Integer methods follow the naming convention write{Signedness}{Bits}{Endianness},
* e.g. writeInt16LE for a signed 16-bit little-endian integer.
*
* All write methods return the number of bytes written, as forwarded from the stream.
*/
class BinaryWriter
{
public function __construct(protected StreamInterface $stream)
{
}
/**
* Returns the current byte offset of the stream cursor.
*/
public function tell(): int
{
return $this->stream->tell();
}
/**
* Moves the stream cursor to the given position.
*
* @param int $whence SEEK_SET, SEEK_CUR, or SEEK_END
*/
public function seek(int $position, int $whence = SEEK_SET): void
{
$this->stream->seek($position, $whence);
}
/**
* Writes raw bytes to the stream.
*/
public function write(string $data): int
{
return $this->stream->write($data);
}
/**
* Writes an unsigned 8-bit integer (0255).
*/
public function writeUInt8(int $value): int
{
return $this->write(\chr($value & 0xff));
}
/**
* Writes a signed 8-bit integer (-128127).
*/
public function writeInt8(int $value): int
{
return $this->writeUInt8($value < 0 ? $value + 0x100 : $value);
}
/**
* Writes an unsigned 16-bit little-endian integer.
*/
public function writeUInt16LE(int $value): int
{
return $this->write(\pack('v', $value));
}
/**
* Writes an unsigned 16-bit big-endian integer.
*/
public function writeUInt16BE(int $value): int
{
return $this->write(\pack('n', $value));
}
/**
* Writes a signed 16-bit little-endian integer.
*/
public function writeInt16LE(int $value): int
{
return $this->writeUInt16LE($value < 0 ? $value + 0x1_0000 : $value);
}
/**
* Writes a signed 16-bit big-endian integer.
*/
public function writeInt16BE(int $value): int
{
return $this->writeUInt16BE($value < 0 ? $value + 0x1_0000 : $value);
}
/**
* Writes an unsigned 32-bit little-endian integer.
*/
public function writeUInt32LE(int $value): int
{
return $this->write(\pack('V', $value));
}
/**
* Writes an unsigned 32-bit big-endian integer.
*/
public function writeUInt32BE(int $value): int
{
return $this->write(\pack('N', $value));
}
/**
* Writes a signed 32-bit little-endian integer.
*/
public function writeInt32LE(int $value): int
{
return $this->writeUInt32LE($value < 0 ? $value + 0x1_0000_0000 : $value);
}
/**
* Writes a signed 32-bit big-endian integer.
*/
public function writeInt32BE(int $value): int
{
return $this->writeUInt32BE($value < 0 ? $value + 0x1_0000_0000 : $value);
}
/**
* Writes a string padded or truncated to exactly $length bytes.
* Writes $data padded or truncated to exactly $length bytes.
*
* Strings shorter than $length are right-padded with $pad_char.
* Strings longer than $length are truncated to $length bytes.
*
* @throws \InvalidArgumentException if $pad_char is not exactly one byte
*/
public function writePaddedString(string $data, int $length, string $pad_char = "\x00"): int
{

View file

@ -10,6 +10,12 @@ use OutOfBoundsException;
use Psr\Http\Message\StreamInterface;
use RuntimeException;
/**
* An in-memory PSR-7 stream backed by a plain PHP string.
*
* Supports both reading and writing. Writing at the current cursor position
* overwrites existing bytes and extends the buffer if the write goes past the end.
*/
class Buffer implements StreamInterface
{
private int $position = 0;
@ -19,11 +25,17 @@ class Buffer implements StreamInterface
{
}
/**
* Detaches and discards the internal string.
*/
public function close(): void
{
$this->detach();
}
/**
* Detaches the internal string, clearing its contents, and returns null.
*/
public function detach(): mixed
{
if ($this->detached) {
@ -37,16 +49,25 @@ class Buffer implements StreamInterface
return null;
}
/**
* Returns the byte length of the buffer, or null after detach.
*/
public function getSize(): ?int
{
return $this->detached ? null : $this->length();
}
/**
* Returns the total byte length of the buffer.
*/
public function length(): int
{
return \strlen($this->data);
}
/**
* Returns the number of bytes between the current cursor position and the end.
*/
public function remaining() : int
{
if ($this->detached) {
@ -56,11 +77,19 @@ class Buffer implements StreamInterface
return $this->length() - $this->position;
}
/**
* Returns true when the cursor is at or past the end of the buffer.
*/
public function eof(): bool
{
return $this->remaining() === 0;
}
/**
* Returns the current byte offset of the cursor.
*
* @throws \RuntimeException if the stream is detached
*/
public function tell() : int
{
$this->ensureAttached();
@ -68,11 +97,22 @@ class Buffer implements StreamInterface
return $this->position;
}
/**
* Always returns true buffers are always seekable.
*/
public function isSeekable(): bool
{
return true;
}
/**
* Moves the cursor to the given position.
*
* @param int $whence SEEK_SET, SEEK_CUR, or SEEK_END
* @throws \InvalidArgumentException for an unrecognised $whence value
* @throws \OutOfBoundsException if the resolved position is outside [0, length]
* @throws \RuntimeException if the stream is detached
*/
public function seek(int $offset, int $whence = SEEK_SET): void
{
$this->ensureAttached();
@ -91,16 +131,30 @@ class Buffer implements StreamInterface
$this->position = $position;
}
/**
* Moves the cursor to the start of the buffer.
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* Always returns true buffers are always readable.
*/
public function isReadable(): bool
{
return true;
}
/**
* Reads up to $length bytes from the current cursor position.
*
* Returns fewer bytes than requested when the end of the buffer is reached.
*
* @throws \InvalidArgumentException if $length is negative
* @throws \RuntimeException if the stream is detached
*/
public function read(int $length): string
{
$this->ensureAttached();
@ -119,16 +173,30 @@ class Buffer implements StreamInterface
return $result;
}
/**
* Always returns true buffers are always writable.
*/
public function isWritable(): bool
{
return true;
}
/**
* Alias for {@see isWritable()}.
*/
public function isWriteable(): bool
{
return $this->isWritable();
}
/**
* Writes $string at the current cursor position, overwriting existing bytes.
*
* If the write extends past the end of the buffer, the buffer grows accordingly.
* Returns the number of bytes written.
*
* @throws \RuntimeException if the stream is detached
*/
public function write(string $string): int
{
$this->ensureAttached();
@ -151,6 +219,11 @@ class Buffer implements StreamInterface
return $length;
}
/**
* Returns all bytes from the current cursor position to the end and advances the cursor.
*
* @throws \RuntimeException if the stream is detached
*/
public function getContents(): string
{
$this->ensureAttached();
@ -165,6 +238,11 @@ class Buffer implements StreamInterface
return $result;
}
/**
* Returns stream metadata, or a single key if $key is provided.
*
* Returns null for unknown keys.
*/
public function getMetadata(?string $key = null): mixed
{
$metadata = [
@ -177,6 +255,9 @@ class Buffer implements StreamInterface
return $key !== null ? $metadata[$key] ?? null : $metadata;
}
/**
* Returns the full buffer contents regardless of cursor position. Returns '' after detach.
*/
public function __toString(): string
{
if ($this->detached) {
@ -186,6 +267,9 @@ class Buffer implements StreamInterface
return $this->data;
}
/**
* @throws \RuntimeException
*/
private function ensureAttached(): void
{
if ($this->detached) {

View file

@ -5,6 +5,9 @@ declare(strict_types=1);
namespace Shufflingpixels\IO\Exception;
/**
* Thrown when a read is attempted past the end of a stream.
*/
class EndOfStreamException extends IOException
{
}

View file

@ -5,6 +5,9 @@ declare(strict_types=1);
namespace Shufflingpixels\IO\Exception;
/**
* Thrown when a stream or file operation fails.
*/
class IOException extends \Exception
{
}

View file

@ -7,10 +7,13 @@ namespace Shufflingpixels\IO;
use Shufflingpixels\IO\Exception\IOException;
/** A PSR-7 stream backed by a file on disk, opened via {@see FileMode}. */
class File extends Resource
{
/**
* @throws IOException
* Opens a file and returns a stream for it.
*
* @throws IOException if the file cannot be opened
*/
public static function open(string $filename, FileMode $mode = FileMode::RW) : self
{

View file

@ -5,23 +5,30 @@ declare(strict_types=1);
namespace Shufflingpixels\IO;
/**
* File open modes passed to {@see File::open()}.
*
* READ read-only, file must exist.
* WRITE write-only, truncates or creates the file.
* RW read/write, file must exist.
*/
enum FileMode : string
{
case READ = 'r';
case WRITE = 'w';
case RW = 'r+';
public function seekable()
public function seekable(): bool
{
return true;
}
public function readable()
public function readable(): bool
{
return $this === self::READ || $this === self::RW;
}
public function writeable()
public function writeable(): bool
{
return $this === self::WRITE || $this === self::RW;
}

View file

@ -12,11 +12,25 @@ use RuntimeException;
use Shufflingpixels\IO\Exception\IOException;
/**
* A read-only PSR-7 stream window over a slice of another seekable stream.
*
* Presents the bytes [$start, $start + $length) of the underlying stream as an
* independent stream with its own cursor starting at offset 0. The underlying
* stream is seeked on every read, so LimitedResource instances over the same
* base stream can be used independently without interfering with each other.
*/
class LimitedResource implements StreamInterface
{
private int $position = 0;
private bool $detached = false;
/**
* @param StreamInterface $stream The underlying stream to read from. Must be seekable.
* @param int $start Byte offset in $stream where this window begins.
* @param int $length Number of bytes this window exposes.
* @throws IOException if $stream is not seekable, or $start or $length are negative
*/
public function __construct(
private StreamInterface $stream,
private int $start,
@ -33,11 +47,17 @@ class LimitedResource implements StreamInterface
}
}
/**
* Detaches from the underlying stream. Does not close it.
*/
public function close(): void
{
$this->detach();
}
/**
* Detaches from the underlying stream and returns null. Does not close it.
*/
public function detach(): mixed
{
if ($this->detached) {
@ -50,16 +70,27 @@ class LimitedResource implements StreamInterface
return null;
}
/**
* Returns the window length in bytes, or null after detach.
*/
public function getSize(): ?int
{
return $this->detached ? null : $this->length;
}
/**
* Returns true when the cursor is at or past the end of the window, or after detach.
*/
public function eof(): bool
{
return $this->detached || $this->position >= $this->length;
}
/**
* Returns the current byte offset within the window (not the underlying stream).
*
* @throws \RuntimeException if the stream is detached
*/
public function tell(): int
{
$this->ensureAttached();
@ -67,11 +98,22 @@ class LimitedResource implements StreamInterface
return $this->position;
}
/**
* Always returns true the window is always seekable.
*/
public function isSeekable(): bool
{
return true;
}
/**
* Moves the cursor to the given position within the window.
*
* @param int $whence SEEK_SET, SEEK_CUR, or SEEK_END (relative to the window, not the underlying stream)
* @throws \InvalidArgumentException for an unrecognised $whence value
* @throws \OutOfBoundsException if the resolved position is outside [0, length]
* @throws \RuntimeException if the stream is detached
*/
public function seek(int $offset, int $whence = SEEK_SET): void
{
$this->ensureAttached();
@ -90,16 +132,31 @@ class LimitedResource implements StreamInterface
$this->position = $position;
}
/**
* Moves the cursor to the start of the window.
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* Always returns true the window is always readable.
*/
public function isReadable(): bool
{
return true;
}
/**
* Reads up to $length bytes from the current cursor position within the window.
*
* Clamps the read to the window boundary so it never reads into adjacent data.
* Returns fewer bytes than requested when the end of the window is reached.
*
* @throws \InvalidArgumentException if $length is negative
* @throws \RuntimeException if the stream is detached
*/
public function read(int $length): string
{
$this->ensureAttached();
@ -120,16 +177,27 @@ class LimitedResource implements StreamInterface
return $data;
}
/**
* Always returns false writing to a window is not supported.
*/
public function isWritable(): bool
{
return false;
}
/**
* @throws IOException always the window is read-only
*/
public function write(string $string): int
{
throw new IOException('LimitedResource is read-only');
}
/**
* Returns all bytes from the current cursor position to the end of the window.
*
* @throws \RuntimeException if the stream is detached
*/
public function getContents(): string
{
$this->ensureAttached();
@ -141,11 +209,19 @@ class LimitedResource implements StreamInterface
return $this->read($this->length - $this->position);
}
/**
* Always returns null no metadata is available for a window stream.
*/
public function getMetadata(?string $key = null): mixed
{
return null;
}
/**
* Returns the full window contents regardless of current cursor position.
*
* Returns '' after detach or if an error occurs during reading.
*/
public function __toString(): string
{
if ($this->detached) {
@ -160,6 +236,9 @@ class LimitedResource implements StreamInterface
}
}
/**
* @throws \RuntimeException
*/
private function ensureAttached(): void
{
if ($this->detached) {

View file

@ -8,6 +8,12 @@ namespace Shufflingpixels\IO;
use Psr\Http\Message\StreamInterface;
use Shufflingpixels\IO\Exception\IOException;
/**
* Base PSR-7 stream implementation wrapping a PHP file resource.
*
* Concrete subclasses supply the resource and declare which capabilities
* (seekable, readable, writable) are available for their use-case.
*/
abstract class Resource implements StreamInterface
{
protected ?int $size = null;
@ -23,6 +29,9 @@ abstract class Resource implements StreamInterface
{
}
/**
* Closes the underlying file resource. Subsequent calls are no-ops.
*/
public function close(): void
{
if ($this->resource !== null) {
@ -31,6 +40,9 @@ abstract class Resource implements StreamInterface
}
}
/**
* Detaches and returns the underlying file resource, leaving the stream unusable.
*/
public function detach(): mixed
{
$resource = $this->resource;
@ -39,6 +51,11 @@ abstract class Resource implements StreamInterface
return $resource;
}
/**
* Returns the byte size of the stream, or null for non-seekable streams.
*
* The result is cached after the first call and invalidated by any write.
*/
public function getSize(): ?int
{
if (!$this->isSeekable()) {
@ -56,21 +73,36 @@ abstract class Resource implements StreamInterface
return $this->size;
}
/**
* Returns true when the underlying resource is at end-of-file.
*/
public function eof(): bool
{
return feof($this->resource);
}
/**
* Returns whether this stream supports seeking.
*/
public function isSeekable(): bool
{
return $this->seekable;
}
/**
* Returns the current byte offset of the file cursor.
*/
public function tell() : int
{
return ftell($this->resource);
}
/**
* Moves the file cursor to the given position.
*
* @param int $whence SEEK_SET, SEEK_CUR, or SEEK_END
* @throws IOException if the stream is not seekable or the seek fails
*/
public function seek(int $position, int $whence = SEEK_SET): void
{
if (!$this->isSeekable()) {
@ -82,26 +114,45 @@ abstract class Resource implements StreamInterface
}
}
/**
* Returns whether this stream supports reading.
*/
public function isReadable(): bool
{
return $this->readable;
}
/**
* Reads up to $length bytes from the current cursor position.
*/
public function read(int $length): string
{
return fread($this->resource, $length);
}
/**
* Returns whether this stream supports writing.
*/
public function isWritable(): bool
{
return $this->writeable;
}
/**
* Alias for {@see isWritable()}.
*/
public function isWriteable(): bool
{
return $this->isWritable();
}
/**
* Writes $string at the current cursor position and returns the bytes written.
*
* Invalidates the cached size so {@see getSize()} reflects the new length.
*
* @throws IOException if the stream is not writable or the write fails
*/
public function write(string $string): int
{
if (!$this->writeable) {
@ -117,16 +168,27 @@ abstract class Resource implements StreamInterface
return $result;
}
/**
* Moves the cursor to the start of the stream.
*/
public function rewind(): void
{
$this->seek(0);
}
/**
* Returns all bytes from the current cursor position to the end.
*/
public function getContents(): string
{
return (string) stream_get_contents($this->resource);
}
/**
* Returns stream metadata, or a single key if $key is provided.
*
* Returns null for unknown keys.
*/
public function getMetadata(?string $key = null): mixed
{
$data = stream_get_meta_data($this->resource);
@ -134,6 +196,9 @@ abstract class Resource implements StreamInterface
return $key !== null ? $data[$key] ?? null : $data;
}
/**
* Returns all remaining stream contents as a string.
*/
public function __toString(): string
{
return $this->getContents();