gdbots/ncr-bundle
A PHP bundle from Gdbots that packages NCR (Nexus Common Runtime) components into a reusable service bundle, providing shared configuration, dependency wiring, and common utilities for applications built on the Gdbots stack.
Know what you're adopting — and which major you can actually install. gdbots/ncr-bundle is a Symfony bundle, not a Laravel package. It contains almost no logic of its own — its job is to wire the gdbots/ncr library (a change-log / node-repository persistence layer built on the gdbots pbj message system) into a Symfony kernel's service container. For a Laravel app that means the value you get is the library plus its reference configuration; the bundle wrapper is something you'll largely re-implement.
Check the version matrix before you type composer require. As of v5.0.0 the bundle requires PHP >= 8.5 and Symfony 8.x. That is a hard floor, not a suggestion:
| Bundle | PHP | Symfony | Realistic Laravel target |
|---|---|---|---|
| v5.x | >= 8.5 | ^8.0 | none today — Laravel 11/12 pin Symfony ^7 |
| v4.x | 8.2–8.4 era | ^7.0 | Laravel 11/12 |
So on a normal Laravel codebase you will either (a) install v4.x, or (b) not install the bundle at all. Decide this first; everything below branches on it.
Install deliberately. Requiring the bundle drags in symfony/framework-bundle, symfony/dependency-injection, and the Config component — which will collide with the symfony/* versions Laravel already requires. If you only want the persistence behavior, require the underlying library instead and treat the bundle as documentation:
composer require gdbots/ncr # the actual engine
composer require --dev gdbots/ncr-bundle:^4.0 # read its service wiring as a reference
If you do install the bundle, pin an exact version ("gdbots/ncr-bundle": "5.0.0") — this is a zero-star, zero-dependent package, so a broken tag is yours to absorb. Before committing, run composer why-not gdbots/ncr-bundle 5.0.0 to see exactly which of your existing constraints (usually symfony/dependency-injection) is blocking the major.
Where to look first. The source is the only reliable documentation. Read in this order:
src/DependencyInjection/Configuration.php — every config key the bundle accepts, with defaults. This is your config/ncr.php blueprint.src/DependencyInjection/*Extension.php — how those keys map to parameters.src/Resources/config/*.xml (or *.yaml) — the actual service definitions, aliases, and which concrete classes get bound to which interfaces.CompilerPass classes — they reveal the intended decoration/extension points.gdbots/ncr source) — the API you'll actually call.First use case. The canonical flow is: append a change/message for a node, then read it back by node reference or by timeline. Write a throwaway test that constructs the concrete repository by hand (no container), appends one message, and reads it back. If you can't make that work in a raw PHP script, integrating it with Laravel's container won't help.
Pattern A — Skip the bundle, bind the library (default choice, and the only one that ignores v5's constraints). Create a NcrServiceProvider that constructs the library's concrete repository/manager classes from config/ncr.php values and registers them as singletons:
$this->app->singleton(NodeRepository::class, fn ($app) => new DynamoNodeRepository(
client: $app->make(DynamoDbClient::class),
table: config('ncr.table'),
// ...
));
Map the bundle's XML parameters one-for-one into config/ncr.php. You keep Laravel's config caching, env access, and test overrides — you lose nothing, because the bundle was only ever a container recipe. Note the v5 bump does not change this path at all: the library's API is untouched, so reading the v5 Resources/config files as documentation is still valid even if you're running PHP 8.2.
Pattern B — Embed a standalone Symfony container. Only viable if the Symfony major matches what Laravel pins (i.e. v4.x on today's Laravel, or v5.x once the framework catches up). Register the bundle extension into a throwaway ContainerBuilder, compile, and expose the handful of services you need as Laravel bindings:
$c = new ContainerBuilder();
$c->setParameter('kernel.debug', config('app.debug'));
(new NcrExtension())->load($config, $c);
$c->compile();
$this->app->instance(NodeRepository::class, $c->get(NodeRepository::class));
Do this in a provider that is not deferred, and never inside a request hot path — compile once per process, cache the compiled container in bootstrap/cache if boot cost shows up in profiles. Two symfony/dependency-injection majors cannot coexist in one vendor tree, so attempting this with v5 on a Symfony-7 Laravel is an install-time failure, not a runtime one.
Pattern C — Adapter/facade layer (recommended regardless). Never let domain code touch gdbots\* classes directly. Define your own ChangeLogRepository interface in app/Contracts, implement it with a thin adapter over NCR, and bind it. This is what makes the package swappable when you discover its maintenance cadence doesn't match your roadmap, and it's what makes tests fast.
Workflow for writing changes. The gdbots ecosystem is message-first: you define PBJ/Protobuf schemas, generate PHP message classes, and the repository stores those. So the day-to-day loop is schema → generate → persist → query. Set up the codegen step as an explicit Composer script ("pbj:generate": "...") and commit the generated classes; don't generate at deploy time.
Testing. Bind an in-memory or fake implementation of your adapter interface in TestCase::setUp(). Reserve the real NCR-backed binding for a small integration suite tagged @group integration that runs against a local DynamoDB/emulator. Do not let the Symfony container leak into unit tests.
Queues and transactions. Appends are the natural unit here — dispatch a job per append rather than batching arbitrarily, and make the append idempotent (dedupe on change ID) so retries from Laravel's queue are safe.
v5.0.0 is a constraints-only release. The changelog says it plainly: require Symfony 8.x, require PHP 8.5+. No code changes, no new features, no deprecations, no API change. Treat it as a signal about the maintainer's compatibility policy, not as a reason to upgrade — and definitely not as a reason to bump your own composer.json platform requirement just to keep a zero-dependent dev dependency current.
PHP 8.5 is a real deployment constraint. If you genuinely want v5, your CI matrix, Docker base image, and config.platform.php in composer.json all have to move, and so does anything that shares that runtime. For a package that contributes container wiring you're going to re-implement anyway, that is a poor trade. Pin ^4.0 and move on.
Symfony-major collision is the #1 install blocker. Laravel 11/12 require symfony/*: ^7.0. v5 requires ^8.0. Composer will resolve you down to v4 or fail outright depending on your other constraints; expect the error message to blame symfony/dependency-injection rather than the bundle, which makes it easy to misdiagnose. composer why-not first, always.
No Laravel auto-discovery. There's no extra.laravel.providers key, so composer require will do nothing on its own. You must write the provider yourself; don't waste time looking for a publish tag or an install command.
Symfony config ≠ Laravel config. The bundle uses the Symfony Config tree (node builders, defaults, required-node enforcement). Laravel's config() gives you a plain array with no validation. Mirror the important constraints manually in your provider's boot() and throw early — a missing table name should fail at boot, not on first write.
Parameters and the parameter bag. Symfony services reference %ncr.table%-style parameters. When hand-porting definitions, resolve every %...% reference explicitly; silently leaving one unresolved is the most common source of "why is this null" in Pattern A.
PBJ is the real onboarding cost. The single biggest time sink is not the bundle — it's the schema/codegen toolchain required to produce the message classes NCR persists. Protobuf/PBJ tooling, generated namespaces, and version drift between generated code and the library will consume more hours than the integration itself. Prototype that pipeline before committing to the package.
Zero stars, zero dependents, no docs — but actively constraint-bumped. v5 shows someone is tracking Symfony releases, which is mildly reassuring for security-adjacent updates, but there are no downstream users to have caught a bad tag and no changelog history beyond this one line. Treat every class name and config key as unverified until you've read the source, and consider vendoring or forking the two or three files you actually depend on — the surface area is small enough that a fork is cheaper than tracking upstream.
Debugging container errors. If using Pattern B, Symfony's container compile errors are cryptic and reference service IDs you never wrote. Dump $container->getServiceIds() and diff against what you expect; also set $container->setParameter('kernel.debug', true) to get clearer messages. For Pattern A, the failure mode is inverted — silent nulls — so add an assertion per binding in the provider.
Extension points. The bundle's compiler passes tell you what the author intended to be replaceable (typically the storage client and the repository implementation). In Laravel, express the same thing with $this->app->extend(...) or by rebinding the interface after the singleton is registered — but do it in a provider ordered after the NCR provider.
When to walk away. If the prototype in "First use case" takes more than a day, stop. The bundle provides container wiring only — an event-sourced change log is something you can model directly with an Eloquent table and a JSON column at a fraction of the integration risk. The v5 release does nothing to change that calculus; it only shrinks the set of environments where the wrapper is even installable.
How can I help you explore Laravel packages today?