apache/avro
Apache Avro for PHP: a schema-based data serialization framework with compact binary encoding, rich schema evolution, object container files, and RPC support. Use it to exchange structured data reliably between services and languages while keeping payloads small.
Apache Avro is not a Laravel package — there is no service provider, facade, config file, or published assets. It is a low-level serialization library with a global Avro* class set (AvroSchema, AvroDatumWriter, AvroDatumReader, AvroStringIO, AvroDataIO). You wire it into Laravel yourself.
Install and verify the PHP implementation is actually present:
composer require apache/avro
composer show apache/avro # confirm version + PHP constraint
php -r "var_dump(class_exists('AvroSchema'));"
Look first at three entry points:
AvroSchema::parse($json) — compiles an .avsc schema. Reuse the parsed object; parsing is the expensive part.AvroDatumWriter / AvroDatumReader — binary encode/decode of a single datum over an AvroIO stream.AvroDataIO::open_file($path, $mode, $schema, $codec) — Avro Object Container Files (the .avro files with embedded schema and codec).First use case: encode a domain event into a binary payload you can push onto a queue, Kafka topic, or object storage.
use AvroSchema;
use AvroDatumWriter;
use AvroStringIO;
$schema = AvroSchema::parse(file_get_contents(base_path('schemas/order_created.avsc')));
$io = new AvroStringIO();
(new AvroDatumWriter($schema))->write([
'order_id' => $order->id,
'total_cents' => $order->total_cents,
'currency' => 'EUR',
'created_at' => $order->created_at->getTimestampMs(),
'notes' => $order->notes, // null union
], $io);
$payload = $io->string();
Decode by rewinding before reading:
use AvroDatumReader;
$io = new AvroStringIO($payload);
$io->seek(0);
$record = (new AvroDatumReader($schema))->read($io);
Bind it as a singleton so the schema is parsed once per request/worker:
$this->app->singleton(OrderEventCodec::class, fn () => new OrderEventCodec(
AvroSchema::parse(file_get_contents(base_path('schemas/order_created.avsc')))
));
Dedicated codec class per schema. Don't scatter AvroSchema::parse through controllers and jobs. Wrap encode/decode, and validate the record shape before it reaches Avro so you get a useful error instead of AvroTypeException.
final class AvroCodec
{
public function __construct(private \AvroSchema $schema) {}
public function encode(array $record): string
{
$io = new \AvroStringIO();
(new \AvroDatumWriter($this->schema))->write($record, $io);
return $io->string();
}
public function decode(string $payload): array
{
$io = new \AvroStringIO($payload);
$io->seek(0);
return (new \AvroDatumReader($this->schema))->read($io);
}
}
Object Container Files for archival / data lake dumps. Use AvroDataIO::open_file with an explicit codec (deflate is the practical choice; it needs ext-zlib). Files are append-only batches — good for nightly exports, bad for random updates.
$fh = \AvroDataIO::open_file(storage_path("app/avro/orders-{$date}.avro"), 'w', $schemaJson, 'deflate');
foreach ($orders->lazyById(1000) as $order) {
$fh->append($order->toAvroRecord());
}
$fh->close();
Reading back is a generator/array depending on version — always iterate, never assume the whole file fits in memory on a worker.
Schema evolution: pass both schemas. The reader supports writer/reader schema resolution, which is how you roll forward without rewriting old files:
$reader = new \AvroDatumReader($writerSchema, $readerSchema);
Keep .avsc files in resources/schemas/ under version control, with a v1/, v2/ naming convention and additive-only changes (new fields with defaults, never reordered or renamed fields).
Schema registry integration. Avro ships no registry client. If you're on Confluent/Kafka, prepend the magic byte + 4-byte schema ID yourself and cache the parsed schema by ID. The registry lookup should be a Cache::remember in a SchemaRegistry service, not a call inside the producer loop.
Mapper layer between Eloquent and Avro. Avro records are flat-ish typed arrays. Add toAvroRecord() methods or dedicated Data Transfer Objects rather than passing $model->toArray() — column name drift and Carbon objects will break encoding in ways that are hard to trace. Encode timestamps as long (ms) consistently and document the unit in the schema.
Queue integration. For a ShouldQueue event or a job payload, keep Avro binary out of the JSON-serialized job properties. Instead, encode inside handle() and hand the binary to your transport (Kafka producer, S3 PUT, raw HTTP body). Never serialize() a binary Avro blob into a Laravel queued job.
Testing pattern. Round-trip is the only test that matters: encode → decode → assert equality. Freeze schema fixtures in tests/fixtures/avsc/ and add a test that asserts each .avsc parses, so CI catches malformed schemas before deploy.
$decoded = $codec->decode($codec->encode($record));
$this->assertSame($record, $decoded);
Validation rule. If you accept Avro payloads at an endpoint (webhook, consumer), wrap decode in a try/catch for AvroException and return 422; do not let raw library exceptions leak into HTTP responses.
Avro:: alias. Everything is manual container binding and manual path resolution. Budget for a thin wrapper package of your own.AvroSchema etc. live at the root namespace. Older releases require an explicit require_once on the implementation entry file rather than relying on Composer autoload — verify with class_exists('AvroSchema') after composer dump-autoload."42" for an int field fails; null is only valid where a union explicitly includes "null"; enum symbols must be exact strings present in symbols. Normalize in your mapper, not by hoping the writer is lenient.["null", "string"] with "default": "x" is invalid. ["string", "null"] with "default": null is invalid. This is the most common schema parse failure.int/long distinction. PHP ints are platform-sized; writing a value beyond int range into an Avro int (32-bit) field throws. Use long for IDs and timestamps.Carbon. Wrap conversions in your codec and test them explicitly.null and deflate are reliable; snappy typically isn't available in PHP. Files written with an unsupported codec will fail on read with an opaque error — record the codec in file metadata and check it.AvroDataIO::open_file(...)->data() may load or eagerly chunk depending on version. For large files, iterate and use chunked exports; watch memory_get_peak_usage() in a test with a realistic file.AvroTypeException / AvroSchemaParseException give you a field path at best. When debugging, json_encode($record) next to $schema->to_schema_json() (or the raw .avsc) in the exception message saves hours.AvroStringIO instances are not — never share a stream across requests or coroutines. Create a fresh AvroStringIO per encode/decode.aliases, which the PHP implementation supports unevenly — test resolution across old files before committing to a rename..avsc to PHP classes. If you need generated DTOs, expect to write them (or a small generator) yourself.How can I help you explore Laravel packages today?