The ReadBuffer class provides binary deserialization for the RabbitMQ Stream Protocol. It reads big-endian binary data from a buffer and converts it to PHP values.
ReadBuffer is a cursor-based buffer reader that supports:
- All integer types (signed and unsigned, 8-64 bit)
- UTF-8 strings with length prefix
- Binary byte arrays with length prefix
- Arrays of deserializable objects
- Position tracking and navigation
- Peek operations (non-consuming reads)
public function __construct(private readonly string $buffer)Parameters:
$buffer- Binary data to read from
Example:
$buffer = new ReadBuffer($frameData);Throws: UnsupportedPlatformException on a 32-bit PHP build — getUint32(),
getUint64() and getInt64() would return floats above PHP_INT_MAX there,
silently breaking their int return types and the offsets they carry. See
Requirements.
All integer methods read from the current position and advance the cursor.
Reads an unsigned 8-bit integer.
public function getUint8(): intReturns: Integer in range 0 to 255
Throws:
DeserializationException- If buffer underflow (less than 1 byte available)
Example:
$value = $buffer->getUint8(); // e.g., 255Reads an unsigned 16-bit integer (big-endian).
public function getUint16(): intReturns: Integer in range 0 to 65535
Throws:
DeserializationException- If buffer underflow (less than 2 bytes available)
Example:
$value = $buffer->getUint16(); // e.g., 65535Reads an unsigned 32-bit integer (big-endian).
public function getUint32(): intReturns: Integer in range 0 to 4294967295
Throws:
DeserializationException- If buffer underflow (less than 4 bytes available)
Example:
$value = $buffer->getUint32(); // e.g., 4294967295Reads an unsigned 64-bit integer (big-endian).
public function getUint64(): intReturns: Integer in range 0 to PHP_INT_MAX
Throws:
DeserializationException- If buffer underflow (less than 8 bytes available)DeserializationException- If the value exceedsPHP_INT_MAX. A uint64 above the signed limit cannot be represented as a native int; the message includes the raw bytes (uint64 value 0x... at position N exceeds PHP_INT_MAX). This rejects a corrupt or hostile value instead of returning a wrapped negative offset.
Example:
$value = $buffer->getUint64(); // e.g., 9223372036854775807Reads $count consecutive unsigned 64-bit integers (big-endian) with a single
bounds check and a single unpack() call with an explicit repeat count
(unpack('J' . $count, ...)). Use it instead of a loop over
getUint64() on frames that carry an array of ids — a broker confirms thousands
of publishing ids in one PublishConfirm frame.
public function getUint64Array(int $count): arrayParameters:
$count- Number of values to read. Must be 0 or greater;0returns an empty array and consumes nothing.
Returns: Zero-indexed array<int, int> with values in range 0 to PHP_INT_MAX.
Note that unpack('J' . $count, ...) is 1-indexed; this method re-indexes from 0,
like every other array reader in this class.
Throws:
DeserializationException- If$countis negativeDeserializationException- If buffer underflow (less than$count * 8bytes available)DeserializationException- If any value exceedsPHP_INT_MAX, with the same messagegetUint64()uses. The whole array is checked, so a corrupt or hostile value anywhere in it is rejected at the parse site instead of flowing on as a wrapped negative id or offset.
Example:
$count = $buffer->getUint32(); // e.g., PublishingIdsCount
$ids = $buffer->getUint64Array($count); // e.g., [0, 1, 2]Reads a signed 16-bit integer (big-endian, two's complement).
public function getInt16(): intReturns: Integer in range -32768 to 32767
Implementation Note: Reads as unsigned and converts negative values using two's complement. For example, 0xFFFF becomes -1.
Throws:
DeserializationException- If buffer underflow (less than 2 bytes available)
Example:
$value = $buffer->getInt16(); // e.g., -1 (from 0xFFFF)Reads a signed 32-bit integer (big-endian, two's complement).
public function getInt32(): intReturns: Integer in range -2147483648 to 2147483647
Implementation Note: Reads as unsigned and converts negative values using two's complement. For example, 0xFFFFFFFF becomes -1.
Throws:
DeserializationException- If buffer underflow (less than 4 bytes available)
Example:
$value = $buffer->getInt32(); // e.g., -2147483648Reads a signed 64-bit integer (big-endian, two's complement).
public function getInt64(): intReturns: Integer in range PHP_INT_MIN to PHP_INT_MAX
Implementation Note: Reads as unsigned and converts negative values using two's complement. For example, 0xFFFFFFFFFFFFFFFF becomes -1.
Throws:
DeserializationException- If buffer underflow (less than 8 bytes available)
Example:
$value = $buffer->getInt64(); // e.g., PHP_INT_MINReads a UTF-8 string with 16-bit length prefix.
public function getString(): ?stringReturns: String value, or null if length is -1 (0xFFFF)
Encoding:
- Length is read as signed 16-bit integer
- Length -1 (0xFFFF) represents null
- Maximum length: 32767 bytes
- Content is read as raw bytes (assumed UTF-8)
Throws:
DeserializationException- If buffer underflow
Example:
$string = $buffer->getString(); // e.g., "Hello, World!"
$nullString = $buffer->getString(); // null (if length was -1)Reads binary data with 32-bit length prefix.
public function getBytes(): ?stringReturns: Binary data, or null if length is -1 (0xFFFFFFFF)
Encoding:
- Length is read as signed 32-bit integer
- Length -1 (0xFFFFFFFF) represents null
- Maximum length: 2147483647 bytes
Throws:
DeserializationException- If buffer underflow
Example:
$data = $buffer->getBytes(); // e.g., "\x00\x01\x02\x03"
$nullData = $buffer->getBytes(); // null (if length was -1)Reads an array of objects from the buffer.
/**
* @template T of FromStreamBufferInterface
* @param class-string<T> $class
* @return array<int, T>
*/
public function getObjectArray(string $class): arrayParameters:
$class- Fully-qualified class name implementingFromStreamBufferInterface
Returns: Array of deserialized objects
Encoding:
- Array length as uint32
- Each item deserialized via
fromStreamBuffer()
Throws:
DeserializationException- If buffer underflow or deserialization fails
Example:
use CrazyGoat\RabbitStream\VO\KeyValue;
// Each element of the array is deserialized with KeyValue::fromStreamBuffer().
$properties = $buffer->getObjectArray(KeyValue::class);Reads an array of strings from the buffer.
/**
* @return array<int, string|null>
*/
public function getStringArray(): arrayReturns: Array of strings (may contain null values)
Encoding:
- Array length as uint32
- Each string decoded with
getString()
Throws:
DeserializationException- If buffer underflow
Example:
$streams = $buffer->getStringArray(); // e.g., ['stream1', 'stream2']Returns the current cursor position.
public function getPosition(): intReturns: Current position in bytes (0 = start of buffer)
Example:
$pos = $buffer->getPosition(); // e.g., 42Advances the cursor by the specified number of bytes.
public function skip(int $bytes): voidParameters:
$bytes- Number of bytes to skip
Throws:
DeserializationException- If skipping would exceed buffer bounds
Example:
$buffer->skip(10); // Skip 10 bytesResets the cursor to the beginning of the buffer.
public function rewind(): voidExample:
$buffer->rewind(); // Position is now 0Reads a fixed number of raw bytes.
public function readBytes(int $length): stringParameters:
$length- Number of bytes to read
Returns: Raw binary data
Throws:
DeserializationException- If buffer underflow
Example:
$raw = $buffer->readBytes(16); // Read 16 raw bytesReads all remaining bytes from the current position to the end.
public function getRemainingBytes(): stringReturns: All remaining buffer content
Throws:
DeserializationException- If position is past buffer end
Example:
$remaining = $buffer->getRemainingBytes();
// Position is now at end of bufferReads an unsigned 16-bit integer without advancing the cursor.
public function peekUint16(): intReturns: Integer in range 0 to 65535
Throws:
DeserializationException- If buffer underflow (less than 2 bytes available)
Example:
$key = $buffer->peekUint16(); // Peek at command key
// Position unchangedThrown for the following conditions:
- Buffer underflow: Attempting to read more bytes than available
- Unpack failure: PHP's
unpack()failed (rare, indicates corrupted data) - Position past end: Cursor position exceeds buffer length
- Object deserialization failure:
fromStreamBuffer()returned null - uint64 out of range: A
getUint64()orgetUint64Array()value exceedsPHP_INT_MAXand cannot be represented as a native signed int
Error Message Format:
Buffer underflow: need {needed} bytes at position {position}, but only {available} available
Buffer underflow: position {position} is past buffer end {end}
Failed to unpack {type} at position {position}
Failed to deserialize object of class {class}
uint64 value 0x{raw bytes} at position {position} exceeds PHP_INT_MAX
use CrazyGoat\RabbitStream\Buffer\ReadBuffer;
use CrazyGoat\RabbitStream\Enum\KeyEnum;
$buffer = new ReadBuffer($frameData);
$key = $buffer->getUint16(); // Command key (e.g., 0x8015 for OpenResponse)
$version = $buffer->getUint16(); // Protocol version
$correlationId = $buffer->getUint32(); // Correlation ID
$responseCode = $buffer->getUint16(); // Response code (1 = OK)
// Map key to enum
$command = KeyEnum::fromStreamCode($key);
echo "Received: {$command->name}\n";$buffer = new ReadBuffer($data);
// Read header
$magic = $buffer->getUint16();
$flags = $buffer->getUint8();
// Read string
$clientName = $buffer->getString();
// Read array of properties
$propertyCount = $buffer->getUint32();
for ($i = 0; $i < $propertyCount; $i++) {
$key = $buffer->getString();
$value = $buffer->getString();
$properties[$key] = $value;
}$buffer = new ReadBuffer($data);
$startPos = $buffer->getPosition(); // 0
$key = $buffer->getUint16(); // Reads 2 bytes
$version = $buffer->getUint16(); // Reads 2 more bytes
currentPos = $buffer->getPosition(); // 4
// Skip correlation ID for now
$buffer->skip(4);
// Read response code
$responseCode = $buffer->getUint16();
// Go back and read correlation ID
$buffer->rewind();
$buffer->skip(4); // Skip key + version
$correlationId = $buffer->getUint32();$buffer = new ReadBuffer($data);
// String that might be null
$streamName = $buffer->getString();
if ($streamName === null) {
echo "Stream name is null\n";
} else {
echo "Stream: $streamName\n";
}
// Binary data that might be null
$metadata = $buffer->getBytes();
if ($metadata === null) {
echo "No metadata\n";
}use CrazyGoat\RabbitStream\Enum\KeyEnum;
$buffer = new ReadBuffer($frameData);
// Peek at command key without consuming
$key = $buffer->peekUint16();
// Check if it's a server-push frame
$isServerPush = in_array($key, [
KeyEnum::PUBLISH_CONFIRM->value,
KeyEnum::PUBLISH_ERROR->value,
KeyEnum::DELIVER->value,
], true);
if ($isServerPush) {
// Handle server-push frame
} else {
// Now actually read the key and continue
$key = $buffer->getUint16();
// ... process response
}use CrazyGoat\RabbitStream\Exception\DeserializationException;
try {
$buffer = new ReadBuffer($incompleteData);
$value = $buffer->getUint32(); // May throw if < 4 bytes
} catch (DeserializationException $e) {
echo "Parse error: " . $e->getMessage() . "\n";
// "Buffer underflow: need 4 bytes at position 0, but only 2 available"
}use CrazyGoat\RabbitStream\Buffer\ReadBuffer;
use CrazyGoat\RabbitStream\Buffer\FromStreamBufferInterface;
class KeyValue implements FromStreamBufferInterface
{
private string $key;
private ?string $value;
public static function fromStreamBuffer(ReadBuffer $buffer): ?object
{
$obj = new self();
$obj->key = $buffer->getString();
$obj->value = $buffer->getString();
return $obj;
}
public function getKey(): string { return $this->key; }
public function getValue(): ?string { return $this->value; }
}
// Read array of KeyValue objects
$properties = $buffer->getObjectArray(KeyValue::class);
foreach ($properties as $prop) {
echo "{$prop->getKey()}: {$prop->getValue()}\n";
}use CrazyGoat\RabbitStream\ResponseBuilder;
$buffer = new ReadBuffer($frameData);
// Read fixed header
$key = $buffer->getUint16();
$version = $buffer->getUint16();
$correlationId = $buffer->getUint32();
// Everything else is the payload
$payload = $buffer->getRemainingBytes();
// Or let the library's dispatcher decode the whole frame for you:
$response = ResponseBuilder::fromResponseBuffer(new ReadBuffer($frameData));