Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Buffer Laravel Package

phpinnacle/buffer

A lightweight PHP library for working with binary data, offering a fluent Buffer API to read and write integers, strings, floats, and more. Useful for protocol clients, parsers, and AMQP-style message handling.

Deep Wiki
Context7

phpinnacle/buffer — Developer Assessment

Getting Started

Install

composer require phpinnacle/buffer

No service provider, no config file, no facade — this is a plain library, which is exactly what you want from a byte-level dependency.

Where to look first

The package is essentially one class: src/ByteBuffer.php, plus a single exception type. Read it end-to-end before writing any integration code (it takes minutes). Everything you need to know about endianness defaults, offset semantics, and range behaviour is in that file. Do not trust memory or blog snippets for the method signatures — the surface is small enough that reading the source is faster than guessing.

Your first use case

Almost every real use is "parse a fixed-size header, then take the variable-length body." A minimal first exercise:

use Pinnacle\Buffer\ByteBuffer;

$raw = $request->getContent();            // e.g. a binary webhook payload

$buffer = ByteBuffer::fromString($raw);  // or: new ByteBuffer($raw)

$version = $buffer->readUInt8(0);        // byte 0
$flags   = $buffer->readUInt16(1);       // bytes 1-2, big-endian by default
$length  = $buffer->readUInt32(3);       // bytes 3-6
$body    = $buffer->bytes(7, $length);   // slice without consuming

if ($buffer->size() !== 7 + $length) {
    throw new \RuntimeException('Malformed frame');
}

Two ideas to internalise immediately, because they govern the rest of the API:

  1. read*() methods take an explicit offset and are non-destructive — you can re-read the same field twice.
  2. consume($n) is the destructive counterpart — it drops the first n bytes.

Getting those two mixed up is the single most common source of "my parser works on the first frame and then desyncs" bugs.

Implementation Patterns

1. A dedicated codec class per wire format

Don't scatter readUInt* calls through controllers and jobs. Wrap each format in one small class that owns the offsets:

namespace App\Binary;

use Pinnacle\Buffer\ByteBuffer;

final class FrameCodec
{
    private const HEADER_SIZE = 7;

    public function encode(int $type, int $channel, string $payload): string
    {
        return (new ByteBuffer())
            ->writeUInt8($type)
            ->writeUInt16($channel)                 // MSB, matches network order
            ->writeUInt32(\strlen($payload))
            ->append($payload)
            ->writeUInt8(0xCE)
            ->bytes();
    }

    /** @return array{type:int, channel:int, payload:string} */
    public function decode(string $raw): array
    {
        $buffer = ByteBuffer::fromString($raw);

        if ($buffer->size() < self::HEADER_SIZE) {
            throw new MalformedFrame('Frame shorter than header');
        }

        $type    = $buffer->readUInt8(0);
        $channel = $buffer->readUInt16(1);
        $length  = $buffer->readUInt32(3);

        if ($buffer->size() < self::HEADER_SIZE + $length) {
            throw new MalformedFrame('Truncated payload');
        }

        return [
            'type'    => $type,
            'channel' => $channel,
            'payload' => $buffer->bytes(self::HEADER_SIZE, $length),
        ];
    }
}

Bind it in a service provider ($this->app->singleton(FrameCodec::class)) and inject it. This is the pattern that keeps the library out of your domain code — if you ever swap to pack()/unpack() or a different buffer lib, one file changes.

2. A cursor wrapper for sequential formats

The offset-passing style is great for headers (random access) and awkward for sequential records. Add a thin cursor on top so you never do arithmetic twice:

final class BinaryReader
{
    private int $offset = 0;

    public function __construct(private readonly ByteBuffer $buffer) {}

    public function u8(): int
    {
        return $this->buffer->readUInt8($this->offset++);
    }

    public function u16(int $order = ByteBuffer::MSB): int
    {
        return $this->buffer->readUInt16($this->offset += 2, 0) ?? 0; // see note below
    }
}

When you write this, prefer the "read then advance" form so an exception leaves the cursor untouched:

public function u16(): int
{
    $value = $this->buffer->readUInt16($this->offset);
    $this->offset += 2;
    return $value;
}

3. Length-prefixed strings are DIY

There are no typed string helpers — strings are bytes. Compose the convention yourself and use it everywhere, or you'll end up with two incompatible encodings in one codebase:

private function writeString(ByteBuffer $b, string $value): void
{
    $b->writeUInt32(\strlen($value))->append($value);
}

/** @return array{0:string, 1:int} value and next offset */
private function readString(ByteBuffer $b, int $offset): array
{
    $length = $b->readUInt32($offset);
    return [$b->bytes($offset + 4, $length), $offset + 4 + $length];
}

Decide once: prefix length in bytes (almost always correct) not characters. strlen() is right; mb_strlen() would be a bug.

4. Make endianness explicit, even when it's the default

The default is big-endian/network order, which is convenient for AMQP and most RFC protocols but wrong for a lot of file formats and anything Windows-derived. Two habits:

  • Pass the order constant explicitly on every read/write of a multi-byte field, even when it matches the default. It makes the code self-documenting and survives a future default change.
  • Encode the per-format choice in the codec class, not at call sites — one const ORDER = ByteBuffer::LSB; inside MyFormatCodec.
$buffer->writeUInt32($len, null, ByteBuffer::LSB);  // null offset = append

5. Test with hex fixtures

Binary code is untestable if you build inputs by hand. Store fixtures as hex strings and round-trip:

public function test_decodes_channel_and_length(): void
{
    $buffer = ByteBuffer::fromString(\hex2bin('01000100000004') . 'test');

    $this->assertSame('0100010000000474657374', $buffer->hex());

    $codec = new FrameCodec();
    $frame = $codec->decode($buffer->bytes());

    $this->assertSame(1, $frame['channel']);
    $this->assertSame('test', $frame['payload']);
}

hex() is your dd() equivalent for binary — always assert on it before asserting on decoded values, so a failure tells you whether the bytes or the parsing are wrong.

6. Where it fits in a Laravel app

Good uses: decoding binary frames in a webhook/queue consumer, building AMQP/STOMP payloads, parsing fixed-format file headers on upload, implementing a small binary protocol client in an artisan command.

Poor uses: general-purpose serialization (use JSON/serialize()), caching (use a real serializer), or hot loops over megabytes of data (see Gotchas on memory). Keep it at the I/O boundary: decode into arrays/DTOs immediately, then hand plain PHP data to the rest of the app.

Also: if you're pulling in phpinnacle/amqp, this library comes along as a transitive dependency — don't add a second copy of your own byte handling alongside it.

Gotchas and Tips

read*() vs consume() is the main footgun. Read methods are positional and idempotent; consume() mutates. If you consume() then read at offset 0, you're reading a different field than you think. Pick one style per codec and stick to it — random-access reads for headers, cursor wrapper for sequential bodies. Never mix.

Bounds errors are exceptions, not warnings. Reading past the end throws (the package ships its own exception class — catch it and translate to your domain error, e.g. a 400 for a malformed webhook). Crucially, validate length fields before slicing: readUInt32() giving you 4_000_000_000 and then calling bytes($offset, $length) on a 12-byte buffer must be a rejection you control, not an unhandled 500.

64-bit unsigned is the classic trap. PHP ints are signed 64-bit. Values above PHP_INT_MAX can't round-trip through readUInt64() as a plain int. If your protocol can emit such values (hashes, IDs, timestamps in nanoseconds), handle them as raw bytes or via gmp/bcmath. This is also the reason the library isn't usable on 32-bit PHP for 64-bit fields at all.

Signed/unsigned range checking is on you. Writing a value outside the declared width is a programmer error; don't rely on the library to be lenient. For anything user-derived, validate against the width before writing — the exception (or silent pack() wrap, depending on the path) is much harder to debug downstream.

Writing at an explicit offset patches, it doesn't insert. If you need to backfill a length prefix after building a body, the standard trick is: reserve the space first (writeUInt32(0)), build the payload, then patch the offset once you know the size. Don't assume writeUInt32($v, 3) shifts bytes around — it overwrites in place.

Mutability and shared references. Write methods return $this for fluent chaining, meaning the same object is mutated. If two collaborators hold the same ByteBuffer, one's appends are visible to the other. clone when you need to branch a partially-built buffer.

Memory and appends. The buffer is a plain PHP string; appending repeatedly reallocates. For payloads above a few hundred KB, build the body with normal string concatenation and append() it once — or skip the library entirely and use pack()/streams. It's a framing tool, not a streaming writer; there is no incremental write-to-socket path.

Defaults hide endianness bugs until integration. Big-endian default means little-endian formats "almost work" for small values and blow up at the first multi-byte field with a high byte set. Test with values like 0x0100 and 0x01020304, not 1 and 2 — small numbers mask byte-order errors under every scheme.

Floats are IEEE-754 32/64-bit only. Round-tripping a PHP float through a 32-bit field loses precision. If your protocol stores fixed-point or scaled integers, do the arithmetic in PHP and write integers; don't assume readFloat()/writeFloat() will preserve your decimal money values.

size() vs strlen(). After any consume(), size() reflects the remaining bytes, not the original. Log both when debugging desync — the gap tells you how far the parser drifted.

Last release was 2021 — pin it. The API is small and stable, so the lack of churn is a feature, not a red flag. But pin to a specific minor ("phpinnacle/buffer": "^1.0") and treat upgrades as deliberate, since 24 stars means you are effectively the QA team. The upside: one file to read if you ever need to fork or vendor it.

hex() is the extension point you'll actually use. Log $buffer->hex() at the boundary in local/staging (guard it in production — hex of a 1 MB payload is 2 MB of noise). It turns "the parser is wrong" into a diffable string.

Don't subclass it. Wrap it. The class is small, non-final, and easy to inherit from, but inheritance couples your domain to the library's quirks; a composed FrameCodec/BinaryReader gives you the same ergonomics and a clean swap path.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
laravel2580/idehelpers
delatbabel/viewpages
anavel/gettext
muhamadrezaar/highcharts
delatbabel/keylists
frankkessler/salesforce-laravel-oauth2-rest
anavel/foundation
wdmtech/enviricons
eii/laravel-captcha
muka/shape-reader
tdt/json
refinery29/php-cs-fixer-config
hotmeteor/eco-env
mvaliolahi/hydrate
wearede/tbcpay-php
parsnick/steak
thibaud-dauce/mattermost-php
multilinguals/apollo-client
antaresbr/collection
kr0lik/phpstan-rules