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

Pbjx Bundle Laravel Package

gdbots/pbjx-bundle

Symfony bundle for integrating Gdbots PBJX with Symfony applications. It registers PBJX services, message bus and event-sourcing components, and configuration so you can wire PBJ schemas and messaging into your app with less boilerplate.

View on GitHub
Deep Wiki
Context7

Getting Started

gdbots/pbjx-bundle is a thin Symfony integration layer around gdbots/pbjx. The bundle itself mostly does DI work: it registers the command bus, event bus, request bus, event store, and the compiler passes that collect your handlers into those buses. You bring the messages (generated by gdbots/pbj, from .proto schemas) and the handlers. Nothing works until you have both.

Platform check comes before anything else. As of v5.0.0 the bundle requires Symfony 8.x and PHP 8.5+. There is no feature work in that release — it is purely a platform floor raise. If your app is on Symfony 6/7 or PHP 8.2–8.4, you cannot use v5; pin the 4.x line ("gdbots/pbjx-bundle": "^4.0") and accept that you are on an unmaintained branch. Do this check in a scratch branch before you write a single handler.

Minimal path to a running system:

  1. Install and register the bundles. You almost certainly want the pbj side too, since the schema compiler lives there rather than in pbjx:

    composer require gdbots/pbjx-bundle:^5.0 gdbots/pbj-bundle
    

    Composer will refuse to resolve unless the platform is PHP 8.5+ and Symfony 8.x; read the resolution error rather than forcing --ignore-platform-reqs. Then in config/bundles.php:

    Gdbots\Bundle\PbjBundle\PbjBundle::class      => ['all' => true],
    Gdbots\Bundle\PbjxBundle\PbjxBundle::class    => ['all' => true],
    
  2. Compile your messages. Write .proto files using pbj conventions (each message gets a CURIE like acme:blog:post-created:v1), point the pbj config at your schema dir, and run the compile command (bin/console list pbj to find the exact name/options in your installed version). Generated PHP classes must be autoloadable — commit them or generate them in CI as a build step. If this step is skipped, every bus call fails with "message not found" style errors that look like DI problems but aren't.

  3. Configure the minimum. Read vendor/gdbots/pbjx-bundle/src/DependencyInjection/Configuration.php before writing YAML — it is the authoritative list of keys, and the README may lag behind it. The shape is roughly:

    # config/packages/pbjx.yaml
    pbjx:
      event_store:
        default: in_memory   # dev only; swap for your real store in prod
      # transport/bus wiring per Configuration.php
    

    Start synchronous + in-memory. Async transports are a second-day concern.

  4. Write one handler and dispatch it. A command handler is a plain service implementing the pbjx handler interface; the bundle's compiler pass picks it up by tag (autoconfigured by interface, or add the tag manually in services.yaml if your project disables autoconfigure).

    final class CreatePostHandler implements CommandHandler
    {
        public function handleCommand(Command $command): ?Response
        {
            Assert::isInstanceOf($command, CreatePost::class);
            // validate, apply to aggregate / persist, return null or a Response
        }
    }
    
    #[Route('/posts', methods: ['POST'])]
    public function create(Request $request, CommandBus $commandBus): JsonResponse
    {
        $commandBus->dispatch(CreatePost::fromArray($request->toArray()));
        return new JsonResponse(null, 202);
    }
    

    Inject Gdbots\Pbjx\CommandBus (or the pbjx.command_bus service id) — the bundle aliases it.

Where to look first, in order: the repo README → src/DependencyInjection/Configuration.php (config keys) → src/DependencyInjection/Compiler/ (this tells you every service tag the bundle recognises, which is the real "API" of the bundle) → src/Resources/config/ (service ids and their aliases).

First real use case: "HTTP controller dispatches a command → handler validates and appends an event to the event store → an event subscriber updates a projection." Get that loop green in a functional test before adding transports, retries, or multiple buses.

Implementation Patterns

Know which of the three message types you're writing. Commands are intent (may be rejected), events are immutable facts (never rejected, only appended), requests are RPC-style calls expecting a Response. The bus you dispatch on determines the handler type resolved; do not send a command that "returns data" — that's a request, and mixing them is the most common design drift in pbjx codebases.

Handler-per-message, resolved by CURIE, not class. Handlers are keyed by the message's schema CURIE (vendor:package:message:vN). This means version bumps create a new message identity: post-created:v1 and post-created:v2 need separate handlers (or a shared abstract base plus two thin subclasses). When you rev a schema, keep the old handler registered as long as old events exist in the store — replay and projections will hit it.

Thin controllers, fat handlers. Let the controller only deserialize (Message::fromArray()), dispatch, and map the result. Validation that belongs to the domain goes in the handler; transport-level validation (auth, HTTP shape) stays in Symfony. This keeps handlers reusable from console commands and message consumers.

Aggregates for state-changing flows. For anything with a lifecycle (orders, posts, accounts), use pbjx's aggregate + AggregateRepository: load() the stream, call a domain method that validates and calls applyEvent(), then commit(). The repository appends to the event store and dispatches the resulting events. Skip aggregates for simple fire-and-forget commands — forcing every command through an aggregate adds ceremony without benefit.

Subscribers for side effects and projections. Use event subscribers (not direct calls inside handlers) for anything that isn't part of the aggregate's own consistency boundary: search indexing, notifications, read models. This is what makes the async story possible later. Keep subscribers idempotent — you will eventually replay or redeliver.

Cross-cutting concerns via decoration, not inheritance. Logging, transactions, metrics, and retry belong in a decorator around the bus:

App\Pbjx\LoggingCommandBus:
  decorates: pbjx.command_bus
  arguments: ['@App\Pbjx\LoggingCommandBus.inner']

One decorator per concern, composed by the container. This survives bundle upgrades far better than subclassing a bus implementation — and it is exactly what makes an eventual 4.x → 5.x jump survivable, since a v5 upgrade gives you no new seams to hook into.

Async where it earns its keep. Route only high-fanout or slow events (indexing, email, webhooks) to an async transport; keep aggregate-critical events synchronous so the write side stays consistent. Consume with a long-running worker (bin/console list pbjx / list messenger to find the consumer command in your version) under a supervisor, and make sure the worker boots the kernel once but resets services between messages.

Testing pattern that pays off. In a functional test, let the bundle wire an in-memory event store and synchronous buses, dispatch a command through the container's bus, then assert on the store's stream contents — that's an integration test of your whole message layer without mocking. Unit-test handlers directly with hand-built messages (PostCreated::fromArray([...])); that's where the interesting assertions live.

Pin the platform in CI, all of it. Because v5 is a platform bump rather than a feature release, the interesting failure mode for your project is not "the bundle broke" but "the bundle's floor moved past our runtime." Bake PHP and Symfony versions into a CI matrix entry that runs composer update --prefer-lowest against the pinned constraint set, so a stray ^8.0 on a transitive dependency surfaces as a build failure rather than a production surprise.

Gotchas and Tips

  • v5.0.0 is a platform bump, not a feature release. The only changes are symfony/* → 8.x and php → 8.5+. There are no new extension points, no new config keys, and no deprecation path — 4.x to 5.x is a hard cliff. Plan the upgrade as an application-platform migration (Symfony major + PHP minor), not a bundle bump, and do it as one PR with no other changes so you can bisect.
  • Adoption is effectively zero (0 stars, 0 dependents), and the version history moves in major jumps tied to Symfony majors rather than to features. Treat this as vendor-internal infrastructure: pin exact versions in composer.json, test bundle upgrades deliberately, and expect to read source rather than find blog posts. Budget for the possibility that you become the maintainer — and note that "we are the only consumer" is why a Symfony-8-only release can ship with no deprecation layer.
  • Read composer.json constraints before writing any code, every time you touch this dependency. v5 requires Symfony 8.x and PHP 8.5+. A bundle this niche is unlikely to have broad CI coverage, and a major that exists solely to raise the floor tells you the maintainer tests against exactly one stack.
  • Codegen is a hard prerequisite and a CI hazard. Generated message classes drift silently if someone edits a .proto and forgets to recompile. Add a CI job that recompiles and fails on git diff --exit-code. On the v5 upgrade specifically, regenerate and diff: the generated output should be identical, and any diff is a signal that the pbj side also moved.
  • Optimistic concurrency is the norm. Concurrent appends to the same stream throw. Retry the command (reload the aggregate, re-dispatch), never the event. Put the retry loop in a bus decorator so handlers stay clean.
  • The in-memory event store and transport are dev-only. They lose everything between requests and won't behave under a real worker. Make the production store selection an explicit config item, not a default, so a missing env var fails loudly at boot instead of silently using memory.
  • "No handler found for message" is nearly always one of: the message was generated with a different CURIE version, autoconfigure is disabled and you forgot the tag, or the handler service was removed by a compiler pass. Debug with bin/console debug:container --tag=<tag> (get the exact tag names from src/DependencyInjection/Compiler/) and bin/console debug:container pbjx.command_bus --show-arguments.
  • Don't serialize messages by hand. Never json_encode($message) or stuff a message into a Doctrine JSON column and read it back as an array. Use pbj's own toArray()/fromArray()/toJson() and keep field presence semantics intact; hand-rolled serialization breaks on schema evolution and oneof/any fields.
  • Circular dispatch is easy to create and hard to see. Handler → event → subscriber → command → same handler. Symptom is a stack overflow or a duplicated aggregate append. Draw the message graph for any new event before merging; break cycles with async hops.
  • Watch for missing "default" designations in config. Buses and stores are typically looked up by name with a default fallback; if you define a custom store or transport without designating it default (or without keeping the default defined), you get a container-time error that names a service you never referenced. Expect a wave of these immediately after a Symfony major, since the container/compiler-pass APIs are the usual thing a major release changes.
  • Extension points, in order of usefulness: (1) bus/event-store decorators, (2) custom EventStoreInterface and transport implementations registered under the bundle's service ids, (3) subscribers for cross-message policy, (4) a compiler pass only if you need new tag semantics. Everything above that is a fork — and a fork is the realistic option if you need Symfony 6/7 support that the maintainer has now dropped.
  • Log the CURIE and stream id on every dispatch and every append. When something goes wrong in an event-sourced system, those two values are what let you reconstruct what happened; without them the logs are useless.
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.
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
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin