Infrastructure Contributions
Introduction
LaravelUi5 modules can publish static, cross-cutting facts into a dedicated laravel.ui5/infra/{key} node that Core injects into every UI5 app's served manifest.json. A platform-level fact — the logout URL, the slot catalog, a build identifier — is declared once, by its owner, and reads identically from every app's manifest.
This is what the Infrastructure Contributions surface is for. Before it existed, platform-level facts got smuggled into individual modules' fragments (a portal module putting routes.logout in its own routes, even though logout is not the portal's route). The infra node retires that anti-pattern.
📝 The static-fact rule (load-bearing). Contributions must be user-invariant — the same value for every actor, every request. They may read global state (the registry, named Laravel routes, environment values that are constant per deployment). They may not return per-user, per-partner, per-tenant data. Per-actor identity lives on other channels (the principal/shell channel or the SDK's own contribution mechanism); the infra node is platform-cacheable by construction.
What an infra contribution looks like (read-side)
Every UI5 app's manifest carries the infra node under laravel.ui5:
// GET /ui5/app/{ns}@{ver}/manifest.json → laravel.ui5
{
// … meta, routes, actions, resources, settings, dashboards, shell …
"infra": {
"core": {
"slots": {
"date_from": { "type": "Date", "default": "@first-of-month", "editable": "User", "note": "…" },
"period": { "type": "String", "default": "month", "editable": "User", "note": "…" }
// … the full slot catalog
}
},
"auth": {
"logout": "https://your-app.test/logout"
}
}
}Client-side read path: getManifestEntry('laravel.ui5').infra.core.slots, …infra.auth.logout. Every app's manifest carries the same infra node — that's the point.
Contributing infra from your module
A module contributes by implementing Ui5InfrastructureContributorInterface (one capability interface, two methods):
use LaravelUi5\Core\Ui5\AbstractUi5Module;
use LaravelUi5\Core\Ui5\Capabilities\Ui5InfrastructureContributorInterface;
use LaravelUi5\Core\Ui5\Contracts\Ui5RegistryInterface;
class BuildModule extends AbstractUi5Module
implements Ui5InfrastructureContributorInterface
{
public function getInfrastructureKey(): string
{
// The sub-key under laravel.ui5/infra this module publishes to.
// Keep stable — client code reads `infra.build.…`.
return 'build';
}
public function contribute(Ui5RegistryInterface $registry): array
{
// Read global state freely — environment, named routes, the registry.
// Return user-invariant facts only.
return [
'commit' => env('GIT_COMMIT', 'unknown'),
'tag' => env('APP_VERSION', 'dev'),
'built' => env('BUILD_TIMESTAMP', ''),
];
}
}When the manifest is served, Core's AbstractManifest::buildInfra() discovers this module via instanceof, calls contribute($registry), and lands the result under infra.build. No per-module manifest configuration; the contribution appears in every module's manifest, identically.
The four design rules (what the surface promises)
- Static facts only. The contract is user-invariant — see the static-fact rule above. Returning per-user data poisons every cached manifest in the system.
- The interface is the gate. Implementing
Ui5InfrastructureContributorInterfaceis what makes a module contribute. TheUi5Infrastructuremarker (the auto-registration gate) is separate and not required to contribute — though contributors are infrastructure by nature. - Keys are public client contracts.
getInfrastructureKey()returns a string that client code reads. Pick it stable; don't derive it from your namespace (a namespace rename should not silently break the client). Two contributors with the same key throw aLogicExceptionat manifest-build time. infrais Core-built and not aggregatable. A subclassManifest::contributeFragment()that tries to merge intoinfrais rejected (LogicException("Manifest key [infra] does not allow contributions.")). The only door into the node is the contributor interface.
When not to use the infra node
Any contribution that needs actor/partner/tenant context doesn't belong here. Common examples:
- The current user's display name → belongs on the principal/shell channel, not
infra(per-user data poisons the cache). - A tenant-specific endpoint → belongs on the SDK's own contribution mechanism, which is tenant-aware.
- A partner-scoped permission flag → same: SDK territory.
- A per-organization configuration → SDK territory.
Core's infra surface is bounded by design to platform-level, user-invariant facts. That bound is what makes it manifest-cacheable.
Examples in the wild
| Contributor | Key | Use case | Shape |
|---|---|---|---|
CoreModule | infra.core.slots | The global slot catalog — type/default/editability/note per slot — read by Report Control clients that need to describe selection inputs | Data publication: {slots: {<name>: {type, default, editable, note}}} |
AuthModule (host-side) | infra.auth.logout | The platform logout URL — read by ShellBar clients | Endpoint publication: {logout: "https://…"} |
These are the two reference shapes — data publication (a catalog or descriptor map) and endpoint publication (a URL or handle). Most contributions will fall into one of those two shapes.
How Core wires it (automatic)
LaravelUi5ManifestKeys::INFRA = 'infra'— the manifest key.AbstractManifest::buildInfra()— iteratesUi5RegistryInterface::modules(), filters byinstanceof Ui5InfrastructureContributorInterface, callscontribute(), lands result under the contributor's key. Duplicate-key guard throws. Empty result drops the key entirely.getFragment($module)— includes the result underlaravel.ui5/infra. Same shape for every consuming module.