wapmorgan/binary-stream
A PHP library for reading and parsing binary data from streams and files, with convenient typed readers for integers, strings, and custom structures, plus seeking and position tracking for binary file formats.
wapmorgan/binary-stream is a standalone PHP library — there is no service provider, facade, or config file to publish. Install it with Composer and use the reader/writer classes directly anywhere in your app (controllers, console commands, jobs, service classes):
composer require wapmorgan/binary-stream
Two entry points matter:
wapmorgan\BinaryStream\BinaryStreamReader — opens a file and reads typed fields sequentially.wapmorgan\BinaryStream\BinaryStreamWriter — builds a binary payload field by field.The README's type table (int8/uint8, int16/uint16, int32/uint32, int64/uint64, float, double, byte, char, string:N, cstring, …) is the source of truth for what you can pass to read()/write(). Everything else is a thin wrapper around those type strings.
First use case — parsing the header of a binary file an admin has uploaded, inside a queued job or artisan command:
use wapmorgan\BinaryStream\BinaryStream;
use wapmorgan\BinaryStream\BinaryStreamReader;
use wapmorgan\BinaryStream\BinaryStreamException;
try {
$reader = new BinaryStreamReader(storage_path('app/imports/firmware.bin'));
$reader->setEndian(BinaryStream::LITTLE_ENDIAN);
$signature = $reader->read('string:4'); // "FW01"
$version = $reader->read('uint16');
$recordCnt = $reader->read('uint32');
// ... loop over records, then:
$reader->goToEnd();
} catch (BinaryStreamException $e) {
report($e); // truncated / malformed file
}
Start by opening a small fixture file, reading one field at a time, and dumping values with dd() to confirm endianness and type sizes before writing the real parser.
Keep the layout in one place. Define your packet/file layout once as a class constant array of offset => [type, name] or as a "group" the library can save and reuse (saveGroup() / group() in the reader). Your writer should replay the same map, so read and write can never drift apart. This is the single biggest win over scattering pack('v', ...) / unpack('V', ...) calls through the codebase.
Wrap parsing in a dedicated service + DTO. Don't let BinaryStreamReader leak into controllers. Create e.g. App\Binary\FirmwareHeaderParser::parse(string $path): FirmwareHeader where FirmwareHeader is a readonly/typed value object. Throw a domain-specific InvalidFirmwareException that wraps BinaryStreamException. This keeps the third-party API (which is old and unmaintained) at one replaceable seam.
Bridge Laravel storage to the reader. The reader wants a local, seekable file. For S3 or uploaded files:
$tmp = tempnam(sys_get_temp_dir(), 'bs_');
file_put_contents($tmp, Storage::disk('s3')->get($key));
try {
$reader = new BinaryStreamReader($tmp);
// ...
} finally {
@unlink($tmp);
}
For very large files, prefer a chunked job: read a fixed-size record, dispatch a child job with the decoded record, then goTo($recordStart + $recordSize) and continue — rather than materialising the whole file into an array.
Generate binary outbound payloads with the writer, not with string concatenation. This is where the library pays for itself on things like WebSocket frames, custom TCP/UDP payloads, .ico/font/archive building, or hardware protocol simulators:
$writer = new BinaryStreamWriter($path);
$writer->setEndian(BinaryStream::BIG_ENDIAN);
$writer->write('string:4', 'FW01');
$writer->write('uint16', $version);
$writer->write('uint32', count($records));
$writer->save();
Use it in tests as a fixture generator. Round-trip tests are the cheapest correctness check you'll get: write a payload with BinaryStreamWriter, parse it with BinaryStreamReader, assert the DTO equals the original. Keep small .bin fixtures in tests/fixtures/ under version control rather than generating everything at runtime.
Custom types are the extension point. Built-in types won't cover things like BCD, packed bitfields, or length-prefixed arrays of structs. Look at the Types source directory and mirror an existing type's read/write contract rather than trying to shoehorn string/int combos into your layout — the latter quickly becomes unreadable.
setEndian() on the same line as the constructor so it's impossible to miss during review.string:N is bytes, not characters. Multi-byte encodings will bite you; convert with mb_convert_encoding($raw, 'UTF-8', 'Windows-1252') after reading if the source is legacy. cstring vs string:N differ in null-termination and padding — off-by-one errors here usually surface as a shifted record count, not an obvious failure.goTo(), goToEnd(), and getPosition() need a seekable stream. php://input and some HTTP/S3 streams are not — spool to php://temp or a temp file first.uint64/int64 on a 32-bit runtime, cast to string and use bcmath/gmp for arithmetic.while ($reader->getPosition() < $reader->getLength()) check (or track expected record count from the header) so a malformed upload fails fast with your domain exception instead of reading zeroed/garbage bytes.$flags & 0x01, ($flags >> 1) & 0x03) in a small helper and unit-test it separately.float is 32-bit, double is 64-bit. Pick deliberately for sensor/telemetry formats; a mismatch produces plausible-looking but wrong numbers rather than errors.App\Binary\ namespace, which also makes it trivial to swap for pack()/unpack() later if the package goes stale.How can I help you explore Laravel packages today?