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

Pbj Laravel Package

gdbots/pbj

gdbots/pbj brings Protocol Buffers to PHP with schema-driven message classes, built-in validation, and serialization to JSON, binary, and protobuf. Define a schema once, generate PHP classes, and move data reliably between services with a consistent, type-safe format.

View on GitHub
Deep Wiki
Context7

Getting Started

This package is a runtime, not a framework integration — it ships the message base classes, builders, and serializers. The message classes themselves are produced by schema code generation (the Gdbots schema tooling, not this repo). That distinction drives your first hour.

  1. composer require gdbots/pbj
  2. Locate your schemas — you need the Gdbots schema files (JSON) and the pbj codegen CLI to produce PHP classes. Check the repo README for the exact generator package name; don't assume it's bundled.
  3. Generate classes into something like app/Pbj/ or generated/Pbj/ and commit them, then autoload via PSR-4 (or classmap if the generator doesn't namespace consistently).
  4. Read, in this order, in src/: Message.php / AbstractMessage, AbstractMessageBuilder, then Serializer/. Those four files tell you 90% of what you need; external docs are thin.

First working use case — a request DTO:

// app/Http/Controllers/PostController.php
public function store(StorePostRequest $request)
{
    $post = AcmeBlogPostV1::builder()
        ->setTitle($request->string('title'))
        ->setBody($request->string('body'))
        ->addToTags(...$request->array('tags'))
        ->build();          // required-field validation fires here

    $json = app(JsonSerializer::class)->serialize($post);
}

If you only take one thing from this package, take the builder → validate → build → serialize cycle. Everything else is variation on it.

Implementation Patterns

Register the resolver once, in a provider. Both serializers depend on a SchemaResolver, and the resolver must know every message type you serialize — including nested and Any-typed ones. Populate it exhaustively:

// app/Providers/PbjServiceProvider.php
public function register(): void
{
    $this->app->singleton(SchemaResolver::class, function () {
        return new SchemaResolver([
            AcmeBlogPostV1::schema(),
            AcmeBlogCommentV1::schema(),
            AcmeUserV1::schema(),
        ]);
    });

    $this->app->singleton(JsonSerializer::class, fn ($app) =>
        new JsonSerializer($app->make(SchemaResolver::class))
    );

    $this->app->singleton(BinarySerializer::class, fn ($app) =>
        new BinarySerializer($app->make(SchemaResolver::class))
    );
}

Use builders at the boundaries, messages everywhere else. Controllers, jobs, and listeners accept arrays from the outside world; convert once at the edge with Message::fromArray($data) or ::builder(), then pass immutable messages around. Never let an unvalidated array leak past the controller.

Immutability → toBuilder() for changes. This is the single most common workflow:

$edited = $post->toBuilder()
    ->setTitle('Updated')
    ->build();          // new instance, original untouched

Use this for event sourcing, audit logs, and "before/after" diffs — it's cheap and side-effect free.

Queue payloads: binary; HTTP/API: JSON. Binary is compact and canonical for internal transport. JSON is what you return to clients and what humans read in logs:

public function handle(): void
{
    $message = app(BinarySerializer::class)
        ->deserialize($this->payload);   // string of bytes from the job

    // do work with typed accessors
    $title = $message->getTitle();
}

Validate in the FormRequest, construct in the controller. Use Laravel rules for HTTP-shape validation (required|string|max:255), and let the builder enforce schema invariants. Catch the pbj exception at a single point:

try {
    $post = AcmeBlogPostV1::builder()->...->build();
} catch (\Gdbots\Pbj\Exception\GdbotsPbjException $e) {
    throw ValidationException::withMessages(['payload' => $e->getMessage()]);
}

Enums: validate against the generated class, don't hand-roll. Generated enum types expose their values; validate the request with Rule::in(AcmePostStatus::values())-style checks rather than trusting a string, then pass through the builder.

Testing helpers. Build fixtures with builders (not arrays) so required fields stay honest, and assert on toArray() shapes. Derive variants with toBuilder() instead of copying arrays — a fixture that builds is a fixture that validates.

Versioning lives in the curie. Schemas are named acme:blog:post:v1. Additive changes still generally warrant v2 for external consumers; keep the old generated classes autoloadable so previously-persisted blobs deserialize. Store _schema alongside the blob if you're keeping binary around.

Gotchas and Tips

  • No auto-discovery, no artisan commands. There's almost certainly no Laravel ServiceProvider in the package — you write bindings yourself, and codegen is a separate CLI step in CI or a composer script. Treat generated PHP like compiled output: pin the generator version, regenerate in CI, and fail the build on git diff --exit-code against generated/.
  • Required-field errors only surface on build(). withTitle(null) won't complain; the exception appears at the end of the chain. When debugging a validation failure, bisect by calling build() after each with… — or just trust the exception message, which names the field.
  • SchemaResolver misses are the number-one runtime failure. Polymorphic payloads (AnyMessage/DynamicMessage) resolve nested types through the resolver, so a comment embedded in a post needs its own schema registered — on both producer and consumer. Registering only the root type works in tests and blows up in production.
  • JSON here is not canonical Protobuf JSON. It's pbj's own shape (curie-tagged with _schema, different well-known-type encodings). Third-party proto tooling and protoc-generated readers will not interoperate — don't plan on it.
  • Timestamps are strict. Use DateTimeImmutable in UTC, RFC3339 with microsecond precision. Carbon instances with a non-UTC timezone, or now() without an explicit zone, are a common source of silent mismatches and validation errors. Normalize at the request boundary.
  • Immutability surprises people coming from Eloquent. $post->setTitle() doesn't exist; $post->getTitle() does. Every mutation is a new object — that's the feature, but it means "update in place" loops must reassign.
  • Persistence is on you. No Eloquent cast, no migration helpers. A workable pattern: one payload blob column (binary) plus a handful of scalar columns (id, type, created_at) extracted from the message for indexing/querying. Don't try to query into the blob.
  • Debug with toArray()/jsonSerialize(), never binary. Dump the array form; keep _schema visible so you can see which type is actually being resolved.
  • Zero stars, zero dependents, sparse docs. This is maintained in-house at Gdbots and released publicly, so expect breaking changes across minor versions and read src/ rather than trusting blog posts. Pin an exact version in composer.json.
  • Evaluate honestly before committing. The value only materializes if you (a) own the schema, (b) run the codegen pipeline, and (c) exchange messages across a boundary — services, queues, an event store. If you just want typed DTOs inside one Laravel app, spatie/laravel-data or plain readonly classes will get you there with far less ceremony. Reach for pbj when you need schema evolution and cross-language wire compatibility, not just structure.
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.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings