jaumo/avro
jaumo/avro is a PHP implementation of Apache Avro, offering schema parsing and binary encoding/decoding for records, unions, and logical types. It helps PHP services exchange compact, schema-driven data with Kafka, Hadoop, and other Avro-based systems.
Install via Composer (the package is not on Packagist under every mirror—check the GitLab repo for the repositories entry if composer require fails):
composer require jaumo/avro
This is a modernized fork of the legacy apache/avro PHP library: PSR-4 namespaces, drop-in classes under Avro\…, and RPC support that the original never finished cleanly. If you have existing code written against the old Avro* class names, budget a refactor pass.
Where to look first:
src/Schema/ — Schema::parse() is your entry point for every schema string.src/Datum/ — DatumWriter / DatumReader (generic, array-based) and the specific variants.src/IO/ — StringIO, FileIO; these wrap the byte streams the datum classes read/write.src/DataFile/ — DataFileWriter / DataFileReader for the Avro object-container format (.avro files).src/Protocol/ and src/IPC/ — only relevant if you're doing Avro RPC rather than file/Kafka payloads.First use case — round-trip a record through the container format:
use Avro\Schema\Schema;
use Avro\DataFile\DataFileWriter;
use Avro\DataFile\DataFileReader;
use Avro\Datum\DatumWriter;
use Avro\Datum\DatumReader;
use Avro\IO\FileIO;
$schema = Schema::parse('{"type":"record","name":"User","fields":[
{"name":"id","type":"long"},
{"name":"email","type":"string"}
]}');
$io = new FileIO('/tmp/users.avro', 'w');
$writer = new DataFileWriter(new DatumWriter(), $io);
$writer->create($schema);
$writer->append(['id' => 1, 'email' => '[email protected]']);
$writer->close();
$reader = new DataFileReader(new DatumReader(), new FileIO('/tmp/users.avro', 'r'));
foreach ($reader as $record) { /* array */ }
That loop is the fastest way to confirm the package is installed and wired correctly before you touch Kafka.
Schema as the contract, not the model. The library deals in associative arrays keyed by field name. Resist the urge to map every record to a DTO; the generic DatumWriter/DatumReader are the hot path. If you need objects, use SpecificDatumWriter/SpecificDatumReader with generated or hand-written PHP classes, but note this path is far less trodden than the generic one.
Kafka producer/consumer integration. The typical day-to-day use: serialize a payload before Producer::send(), deserialize on consume.
// produce
$bytes = Avro::encode($schema, $payload); // or a StringIO + DatumWriter
$this->producer->send([new Message(['value' => $bytes, 'key' => ...])]);
// consume
$record = Avro::decode($binarySchema, $message->payload);
If you use Confluent Schema Registry, the wire format is magic byte + 4-byte schema id + avro body. You must strip 5 bytes and fetch the writer schema from the registry yourself — this package speaks pure Avro, not the Confluent envelope.
Schema resolution for evolving data. Reader and writer schemas can differ (added fields with defaults, removed fields). The DatumReader supports projecting a writer record onto a reader schema; use this to tolerate schema drift instead of hard-failing on unknown fields. Default values in the schema are what make added fields safe — make sure every new field has one.
Container files for batch/ETL. For daily dumps, DataFileWriter embeds the schema in the file header, so consumers don't need out-of-band schema knowledge. This is the lowest-friction pattern and a good default for internal pipelines.
RPC only if you actually need it. Avro RPC is niche in PHP. If you're evaluating this package because of the RPC claim, verify the transport layer against your needs (HTTP vs socket, handshake, error propagation) before committing — the PHP ecosystem has very little production mileage here. REST/gRPC with Avro payloads is the more common shape.
Testing. Keep fixture .avro files and schema JSON under tests/fixtures/. Round-trip assertions (encode → decode → assertEquals) catch 90% of integration bugs and don't require a broker.
Namespace migration is the #1 trap. Tutorials and Stack Overflow answers overwhelmingly target apache/avro with its AvroDataIO, AvroSchema style names. This fork uses PSR-4 (Avro\Schema\Schema, Avro\DataFile\DataFileIO, etc.). Translating examples by hand is error-prone — grep the repo's own tests for canonical usage.
No bundled code generation. Unlike Java, there's no avro-tools compiler in the box. Specific-record classes must be written by hand or generated externally with avro-tools from another language and adapted. Most teams stay on the generic array path for this reason.
Logical types are partial. decimal, date, timestamp-millis, and friends depend on the version; older forks silently treat them as their underlying bytes/int/long. Check the changelog and write a round-trip test for any logical type you rely on. Decimal in particular tends to require manual scaling on both sides.
Binary strings vs PHP strings. Avro bytes maps to raw PHP strings; if your payload is UTF-8 text you may need string not bytes, and mixing them up produces corrupt output that only fails at read time. Use the schema as the source of truth.
Union types deserialize to the first matching branch. ["null","string"] returns null for nulls but plain strings otherwise; there's no type tag on the PHP side. When a field is a union of multiple records, you must disambiguate yourself based on the writer schema.
Performance on hot paths. The generic reader/writer do a lot of reflection and array walking. For high-throughput consumers, cache parsed Schema objects (parsing the same JSON repeatedly is expensive), reuse DatumReader/DatumWriter instances, and prefer StringIO over file IO when bytes are already in memory.
Error messages are terse. AvroException variants usually say "expected X, got Y" without a field path. Wrap decode calls in a try/catch that logs the raw bytes (base64) and the writer schema id — otherwise debugging a malformed Kafka message is guesswork.
Extension points. Schema subclasses (RecordSchema, ArraySchema, etc.) and the IO interface are the clean seams. Custom IO implementations let you stream directly from Kafka/S3/Guzzle without an intermediate temp file. The DatumReader::read() hooks are where you'd add custom handling for logical types.
Maintenance posture. This is a single-org fork with zero public dependents on Packagist. Pin the version in composer.json, run the repo's test suite against your PHP version during upgrades, and consider vendoring or forking if you hit a bug — upstream PR turnaround to a GitLab-hosted niche library is not something to plan around.
How can I help you explore Laravel packages today?