Skip to content

Repository files navigation

proto2-php

A Protocol Buffers proto2 implementation for PHP: a wire-format runtime plus a protoc plugin that generates typed message classes from .proto files.

Only proto2 syntax is supported — explicit field presence, required/optional/repeated labels, declared defaults, and group fields, with proto3 files rejected by the generator.

Origin

I wanted to consume a GTFS-realtime feed in PHP. GTFS-realtime is a proto2 schema and shows no sign of ever becoming a proto3 one — transit agencies are not exactly known for chasing the bleeding edge. So, PHP proto2 support: how hard could it be?

Turns out proto2 support for PHP is largely abandoned. Faced with a choice between necromancy and just writing the wire format myself, I picked the option that doesn't involve reading 400-comment GitHub issues asking "is this dead?" — so here we are: a bus tracker turned into a protobuf implementation.

Why

The runtime is generic — it works with any proto2 message once given a field descriptor list — but nothing about it assumes protoc as the source. Descriptors can come from a generated class, from parsing a FileDescriptorProto you built yourself, or by hand for a one-off message, as shown below.

Requirements

  • PHP 8.4+
  • protoc on your PATH (only needed to generate code — the runtime itself has no external dependencies)

Installation

composer require aaronlow/proto2-php

Generating message classes

Given a proto2 file:

syntax = "proto2";
package acme.billing;

message Invoice {
  required int32 id = 1;
  optional string customer = 2;
  repeated int32 line_totals = 3 [packed = true];
}

Run protoc with the bundled plugin:

protoc --php2_out=./generated invoice.proto

This produces generated/Acme/Billing/Invoice.php: a final class extending the runtime Message base, with a fields() descriptor and typed get/set/has/clear accessors (add/clear variants too for repeated fields).

Pass a namespace plugin parameter to override the package-derived PHP namespace:

protoc --php2_out=./generated --php2_opt=namespace=Acme\\Billing invoice.proto

Nested proto messages flatten into one class per outer message, joined with _ (Outer.InnerOuter_Inner), since PHP has no nested classes.

Using generated messages

use Acme\Billing\Invoice;

$invoice = new Invoice()
    ->setId(1001)
    ->setCustomer('Jane Doe')
    ->setLineTotals([1500, 2200]);

$bytes = $invoice->serializeToString();

$parsed = Invoice::parseFromString($bytes);
$parsed->getCustomer(); // 'Jane Doe'
$parsed->hasCustomer(); // true — proto2 explicit presence

Using the runtime directly

You don't need protoc to use the runtime — a message is just a class extending Message that returns a Field list:

use Aaronlow\Proto2Php\Runtime\Field;
use Aaronlow\Proto2Php\Runtime\FieldType;
use Aaronlow\Proto2Php\Runtime\Label;
use Aaronlow\Proto2Php\Runtime\Message;

final class Invoice extends Message
{
    public static function fields(): array
    {
        return [
            new Field(1, 'id', FieldType::INT32, Label::REQUIRED),
            new Field(2, 'customer', FieldType::STRING, Label::OPTIONAL),
            new Field(3, 'line_totals', FieldType::INT32, Label::REPEATED, packed: true),
        ];
    }
}

$invoice = new Invoice()->set(1, 1001)->set(2, 'Jane Doe')->add(3, 1500)->add(3, 2200);
$invoice->serializeToString();

A runnable version, with a round trip and isInitialized() check, lives at example/invoice.php:

php example/invoice.php

Message API

Method Behavior
get(int $number) Present value, or the declared/type default if absent (never creates presence)
set(int $number, mixed $value) Sets a singular or repeated (whole-array) field; marks it present
add(int $number, mixed $value) Appends one element to a repeated field
has(int $number) True iff the field was explicitly set or parsed — proto2 explicit presence
clear(int $number) Removes presence
isInitialized() Recursively checks all required fields are present
serializeToString() / parseFromString() / mergeFromString() Wire-format I/O; mergeFromString follows proto2 merge semantics (singular scalars last-wins, singular messages merge field-by-field, repeated appends)
unknownFieldBytes() Raw bytes of fields not in this message's schema, preserved verbatim from parsing
toArray() Present fields as a nested associative array (for JSON export, etc.); absent fields are omitted, not defaulted

Unknown fields — including a field arriving with the wrong wire type — round-trip byte-for-byte through parse/serialize, as proto2 requires.

Project layout

src/
  Runtime/            Wire-format-agnostic message base: presence, merge, serialize
    Wire/             Varint, tag, and length-delimited encode/decode primitives
  Codegen/
    Descriptor/        Hand-written subset of descriptor.proto (FileDescriptorProto, etc.)
                        and the protoc plugin protocol (CodeGeneratorRequest/Response)
    Generator/          Proto-name → PHP-namespace resolution and PHP source emission
    Plugin.php          protoc plugin entry point (stdin/stdout framing)
bin/
  protoc-gen-php2       The executable protoc invokes

Development

composer test       # PHPUnit
composer analyse    # PHPStan (level 7)
composer cs-check    # php-cs-fixer, dry run
composer cs-fix       # php-cs-fixer, apply

Known limitations

  • Proto2 extensions (extend blocks) are not yet implemented — extension field numbers land in unknownFieldBytes() rather than being parsed.
  • Services and RPC definitions are ignored (this is a message library, not an RPC framework).
  • An unrecognized enum value is stored as its raw int rather than routed to unknown fields.

Contributing

Contributions are welcome — this project exists because the alternative was staring at an unmaintained repo, so PRs that keep it from becoming one are especially appreciated. The "Known limitations" above are all fair game (extensions support would be particularly well-received), and so are bug reports on messages that don't round-trip correctly.

Before opening a PR, please make sure composer test, composer analyse, and composer cs-check all pass.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages